Control Windows Forms' NCA

Post Reply
User avatar
Click16
Posts: 1941
Joined: Mon Dec 31, 2007 4:36 am
Location: United States

Control Windows Forms' NCA

Post by Click16 »

Hello everyone, in this tutorial I will show you how to intercept windows forms messages to control the Non-client area for your windows forms applications.

So what is the non client area? Well it is the area of a windows application that you regularly cannot place controls.

Here is a standard windows application form.
Image

Now in this image, the non client area is going to be covered.

Image

First off we will need a list of windows messages. So here are some common windows messages:
  • WM_NULL = 0x0000,
    WM_CREATE = 0x0001,
    WM_DESTROY = 0x0002,
    WM_MOVE = 0x0003,
    WM_SIZE = 0x0005,
    WM_ACTIVATE = 0x0006,
    WM_SETFOCUS = 0x0007,
    WM_KILLFOCUS = 0x0008,
    WM_ENABLE = 0x000A,
    WM_SETREDRAW = 0x000B,
    WM_SETTEXT = 0x000C,
    WM_GETTEXT = 0x000D,
    WM_GETTEXTLENGTH = 0x000E,
    WM_PAINT = 0x000F,
    WM_CLOSE = 0x0010,
    WM_QUERYENDSESSION = 0x0011,
    WM_QUERYOPEN = 0x0013,
    WM_ENDSESSION = 0x0016,
    WM_QUIT = 0x0012,
    WM_ERASEBKGND = 0x0014,
    WM_SYSCOLORCHANGE = 0x0015,
    WM_SHOWWINDOW = 0x0018,
    WM_WININICHANGE = 0x001A,
    WM_DEVMODECHANGE = 0x001B,
    WM_ACTIVATEAPP = 0x001C,
    WM_FONTCHANGE = 0x001D,
    WM_TIMECHANGE = 0x001E,
    WM_CANCELMODE = 0x001F,
    WM_SETCURSOR = 0x0020,
    WM_MOUSEACTIVATE = 0x0021,
    WM_CHILDACTIVATE = 0x0022,
    WM_QUEUESYNC = 0x0023,
    WM_GETMINMAXINFO = 0x0024,
    WM_PAINTICON = 0x0026,
    WM_ICONERASEBKGND = 0x0027,
    WM_NEXTDLGCTL = 0x0028,
    WM_SPOOLERSTATUS = 0x002A,
    WM_DRAWITEM = 0x002B,
    WM_MEASUREITEM = 0x002C,
    WM_DELETEITEM = 0x002D,
    WM_VKEYTOITEM = 0x002E,
    WM_CHARTOITEM = 0x002F,
    WM_SETFONT = 0x0030,
    WM_GETFONT = 0x0031,
    WM_SETHOTKEY = 0x0032,
    WM_GETHOTKEY = 0x0033,
    WM_QUERYDRAGICON = 0x0037,
    WM_COMPAREITEM = 0x0039,
    WM_GETOBJECT = 0x003D,
    WM_COMPACTING = 0x0041,
    WM_COMMNOTIFY = 0x0044,
    WM_WINDOWPOSCHANGING = 0x0046,
    WM_WINDOWPOSCHANGED = 0x0047,
    WM_POWER = 0x0048,
    WM_COPYDATA = 0x004A,
    WM_CANCELJOURNAL = 0x004B,
    WM_NOTIFY = 0x004E,
    WM_INPUTLANGCHANGEREQUEST = 0x0050,
    WM_INPUTLANGCHANGE = 0x0051,
    WM_TCARD = 0x0052,
    WM_HELP = 0x0053,
    WM_USERCHANGED = 0x0054,
    WM_NOTIFYFORMAT = 0x0055,
    WM_CONTEXTMENU = 0x007B,
    WM_STYLECHANGING = 0x007C,
    WM_STYLECHANGED = 0x007D,
    WM_DISPLAYCHANGE = 0x007E,
    WM_GETICON = 0x007F,
    WM_SETICON = 0x0080,
    WM_NCCREATE = 0x0081,
    WM_NCDESTROY = 0x0082,
    WM_NCCALCSIZE = 0x0083,
    WM_NCHITTEST = 0x0084,
    WM_NCPAINT = 0x0085,
    WM_NCACTIVATE = 0x0086,
    WM_GETDLGCODE = 0x0087,
    WM_SYNCPAINT = 0x0088,
    WM_NCMOUSEMOVE = 0x00A0,
    WM_NCLBUTTONDOWN = 0x00A1,
    WM_NCLBUTTONUP = 0x00A2,
    WM_NCLBUTTONDBLCLK = 0x00A3,
    WM_NCRBUTTONDOWN = 0x00A4,
    WM_NCRBUTTONUP = 0x00A5,
    WM_NCRBUTTONDBLCLK = 0x00A6,
    WM_NCMBUTTONDOWN = 0x00A7,
    WM_NCMBUTTONUP = 0x00A8,
    WM_NCMBUTTONDBLCLK = 0x00A9,
    WM_NCXBUTTONDOWN = 0x00AB,
    WM_NCXBUTTONUP = 0x00AC,
    WM_NCXBUTTONDBLCLK = 0x00AD,
    WM_INPUT = 0x00FF,
    WM_KEYFIRST = 0x0100,
    WM_KEYDOWN = 0x0100,
    WM_KEYUP = 0x0101,
    WM_CHAR = 0x0102,
    WM_DEADCHAR = 0x0103,
    WM_SYSKEYDOWN = 0x0104,
    WM_SYSKEYUP = 0x0105,
    WM_SYSCHAR = 0x0106,
    WM_SYSDEADCHAR = 0x0107,
    WM_UNICHAR = 0x0109,
    WM_KEYLAST = 0x0108,
    WM_IME_STARTCOMPOSITION = 0x010D,
    WM_IME_ENDCOMPOSITION = 0x010E,
    WM_IME_COMPOSITION = 0x010F,
    WM_IME_KEYLAST = 0x010F,
    WM_INITDIALOG = 0x0110,
    WM_COMMAND = 0x0111,
    WM_SYSCOMMAND = 0x0112,
    WM_TIMER = 0x0113,
    WM_HSCROLL = 0x0114,
    WM_VSCROLL = 0x0115,
    WM_INITMENU = 0x0116,
    WM_INITMENUPOPUP = 0x0117,
    WM_MENUSELECT = 0x011F,
    WM_MENUCHAR = 0x0120,
    WM_ENTERIDLE = 0x0121,
    WM_MENURBUTTONUP = 0x0122,
    WM_MENUDRAG = 0x0123,
    WM_MENUGETOBJECT = 0x0124,
    WM_UNINITMENUPOPUP = 0x0125,
    WM_MENUCOMMAND = 0x0126,
    WM_CHANGEUISTATE = 0x0127,
    WM_UPDATEUISTATE = 0x0128,
    WM_QUERYUISTATE = 0x0129,
    WM_CTLCOLOR = 0x0019,
    WM_CTLCOLORMSGBOX = 0x0132,
    WM_CTLCOLOREDIT = 0x0133,
    WM_CTLCOLORLISTBOX = 0x0134,
    WM_CTLCOLORBTN = 0x0135,
    WM_CTLCOLORDLG = 0x0136,
    WM_CTLCOLORSCROLLBAR = 0x0137,
    WM_CTLCOLORSTATIC = 0x0138,
    WM_MOUSEFIRST = 0x0200,
    WM_MOUSEMOVE = 0x0200,
    WM_LBUTTONDOWN = 0x0201,
    WM_LBUTTONUP = 0x0202,
    WM_LBUTTONDBLCLK = 0x0203,
    WM_RBUTTONDOWN = 0x0204,
    WM_RBUTTONUP = 0x0205,
    WM_RBUTTONDBLCLK = 0x0206,
    WM_MBUTTONDOWN = 0x0207,
    WM_MBUTTONUP = 0x0208,
    WM_MBUTTONDBLCLK = 0x0209,
    WM_MOUSEWHEEL = 0x020A,
    WM_XBUTTONDOWN = 0x020B,
    WM_XBUTTONUP = 0x020C,
    WM_XBUTTONDBLCLK = 0x020D,
    WM_MOUSELAST = 0x020D,
    WM_PARENTNOTIFY = 0x0210,
    WM_ENTERMENULOOP = 0x0211,
    WM_EXITMENULOOP = 0x0212,
    WM_NEXTMENU = 0x0213,
    WM_SIZING = 0x0214,
    WM_CAPTURECHANGED = 0x0215,
    WM_MOVING = 0x0216,
    WM_POWERBROADCAST = 0x0218,
    WM_DEVICECHANGE = 0x0219,
    WM_MDICREATE = 0x0220,
    WM_MDIDESTROY = 0x0221,
    WM_MDIACTIVATE = 0x0222,
    WM_MDIRESTORE = 0x0223,
    WM_MDINEXT = 0x0224,
    WM_MDIMAXIMIZE = 0x0225,
    WM_MDITILE = 0x0226,
    WM_MDICASCADE = 0x0227,
    WM_MDIICONARRANGE = 0x0228,
    WM_MDIGETACTIVE = 0x0229,
    WM_MDISETMENU = 0x0230,
    WM_ENTERSIZEMOVE = 0x0231,
    WM_EXITSIZEMOVE = 0x0232,
    WM_DROPFILES = 0x0233,
    WM_MDIREFRESHMENU = 0x0234,
    WM_IME_SETCONTEXT = 0x0281,
    WM_IME_NOTIFY = 0x0282,
    WM_IME_CONTROL = 0x0283,
    WM_IME_COMPOSITIONFULL = 0x0284,
    WM_IME_SELECT = 0x0285,
    WM_IME_CHAR = 0x0286,
    WM_IME_REQUEST = 0x0288,
    WM_IME_KEYDOWN = 0x0290,
    WM_IME_KEYUP = 0x0291,
    WM_MOUSEHOVER = 0x02A1,
    WM_MOUSELEAVE = 0x02A3,
    WM_NCMOUSELEAVE = 0x02A2,
    WM_WTSSESSION_CHANGE = 0x02B1,
    WM_TABLET_FIRST = 0x02c0,
    WM_TABLET_LAST = 0x02df,
    WM_CUT = 0x0300,
    WM_COPY = 0x0301,
    WM_PASTE = 0x0302,
    WM_CLEAR = 0x0303,
    WM_UNDO = 0x0304,
    WM_RENDERFORMAT = 0x0305,
    WM_RENDERALLFORMATS = 0x0306,
    WM_DESTROYCLIPBOARD = 0x0307,
    WM_DRAWCLIPBOARD = 0x0308,
    WM_PAINTCLIPBOARD = 0x0309,
    WM_VSCROLLCLIPBOARD = 0x030A,
    WM_SIZECLIPBOARD = 0x030B,
    WM_ASKCBFORMATNAME = 0x030C,
    WM_CHANGECBCHAIN = 0x030D,
    WM_HSCROLLCLIPBOARD = 0x030E,
    WM_QUERYNEWPALETTE = 0x030F,
    WM_PALETTEISCHANGING = 0x0310,
    WM_PALETTECHANGED = 0x0311,
    WM_HOTKEY = 0x0312,
    WM_PRINT = 0x0317,
    WM_PRINTCLIENT = 0x0318,
    WM_APPCOMMAND = 0x0319,
    WM_THEMECHANGED = 0x031A,
    WM_HANDHELDFIRST = 0x0358,
    WM_HANDHELDLAST = 0x035F,
    WM_AFXFIRST = 0x0360,
    WM_AFXLAST = 0x037F,
    WM_PENWINFIRST = 0x0380,
    WM_PENWINLAST = 0x038F,
    WM_USER = 0x0400,
    WM_REFLECT = 0x2000,
    WM_APP = 0x8000
That will work... So we will use this message to control the painting of the non-client area (NCA)
0x0085
This message to calculate the new client area space
0x0083
And this message to tell when the user clicks on a specific part of the non client area
0x0084

So with those three hexadecimal numbers, we can control our non client area! Now how do we use it? Well we first need to create a struct for our Non Client area calculator so here is the struct code:

Code: Select all

/// <summary>
        /// The NCCALCSIZE_PARAMS structure contains information that an application can use 
        /// while processing the WM_NCCALCSIZE message to calculate the size, position, and 
        /// valid contents of the client area of a window. 
        /// </summary>
        [StructLayout(LayoutKind.Sequential)]
        public struct NCCALCSIZE_PARAMS
        {
            public RECT rect0, rect1, rect2;
            public IntPtr lppos;
        }
And the RECT structure that NCCALCSIZE_PARAMS uses:

Code: Select all

[StructLayout(LayoutKind.Sequential)]
        public struct RECT
        {
            public int Left;
            public int Top;
            public int Right;
            public int Bottom;
        }
Now we are ready to mess with windows' messages!

First override the WndProc (Windows Processes) void.

Code: Select all

protected override void WndProc(ref Message m)
        {
            base.WndProc(ref m);
        }
Now create an If statement

Code: Select all

if (m.Msg == 0x0085)
{
//Null Message...
m.Msg = 0x0;
}
else
{
base.WndProc(ref m);
}
And debug that and see what happens...

Image

Woah! Right? Well if you see what we did, it makes sense. We nulled the Windows Paint Client Area message. So it couldn't paint the client area for the form. The controls and stuff work, but you need to find the caption buttons.

Okay, now depending on your parameters depends on the different results.
Let's remove what we have and now change the code, We are now going to change the non client area's size calculations.

Code: Select all

if (m.Msg == 0x0083&& (int)m.WParam == 1)
{
}
else if (m.Msg == 0x0084 && (int)m.Result == 0)
{
}
else
{
base.WndProc(ref m);
}
To the If Message = 0x0083 And ... we will add:

Code: Select all

NCCALCSIZE_PARAMS nccsp = (NCCALCSIZE_PARAMS)Marshal.PtrToStructure(m.LParam, typeof(NCCALCSIZE_PARAMS));

// Adjust the client rectangle to create a new 'client area'
nccsp.rect0.Top += 0;
nccsp.rect0.Bottom += 0;
nccsp.rect0.Left += 0;
nccsp.rect0.Right += 0;
Marshal.StructureToPtr(nccsp, m.LParam, false);
m.Result = IntPtr.Zero;
As of now, we have no client area. If your intention was to re-size the client area, you would just need to adjust the varying rectangles in the nccsp reference.

For Example:

Code: Select all

nccsp.rect0.Top += 30;
nccsp.rect0.Bottom += -8;
nccsp.rect0.Left += 8;
nccsp.rect0.Right += -8;
If you set your variables to the above, you will have a recreation of the original non client area. The only thing different is that the re-size functions don't work. We will get to that right now...

Add these to the form's class so we can get where you clicked.

Code: Select all

/// <summary>
        /// Equivalent to the LoWord C Macro
        /// </summary>
        /// <param name="dwValue"></param>
        /// <returns></returns>
        private static int LoWord(int dwValue)
        {
            return dwValue & 0xFFFF;
        }

        /// <summary>
        /// Equivalent to the HiWord C Macro
        /// </summary>
        /// <param name="dwValue"></param>
        /// <returns></returns>
        private static int HiWord(int dwValue)
        {
            return (dwValue >> 16) & 0xFFFF;
        }
Inside your else if (...) function, put the following code:

Code: Select all

m.Result = HitTestNCA(m.HWnd, m.WParam, m.LParam);
And below your rect and NCCALCSIZE_PARAMS structure put this:

Code: Select all

/// <summary>
        /// Used to figure out where the user clicked...
        /// </summary>
        /// <param name="hwnd"></param>
        /// <param name="wparam"></param>
        /// <param name="lparam"></param>
        /// <returns></returns>
        private IntPtr HitTestNCA(IntPtr hwnd, IntPtr wparam, IntPtr lparam)
        {
            int HTNOWHERE = 0;
            int HTCLIENT = 1;
            int HTCAPTION = 2;
            int HTGROWBOX = 4;
            int HTSIZE = HTGROWBOX;
            int HTMINBUTTON = 8;
            int HTMAXBUTTON = 9;
            int HTLEFT = 10;
            int HTRIGHT = 11;
            int HTTOP = 12;
            int HTTOPLEFT = 13;
            int HTTOPRIGHT = 14;
            int HTBOTTOM = 15;
            int HTBOTTOMLEFT = 16;
            int HTBOTTOMRIGHT = 17;
            int HTREDUCE = HTMINBUTTON;
            int HTZOOM = HTMAXBUTTON;
            int HTSIZEFIRST = HTLEFT;
            int HTSIZELAST = HTBOTTOMRIGHT;
            //Getting the point where the mouse clicked...
            Point p = new Point(LoWord((int)lparam), HiWord((int)lparam));

            Rectangle topleft = RectangleToScreen(new Rectangle(0, 0, 8, 8));

            if (topleft.Contains(p))
                return new IntPtr(HTTOPLEFT);

            Rectangle topright = RectangleToScreen(new Rectangle(Width - 8, 0, 8, 8));

            if (topright.Contains(p))
                return new IntPtr(HTTOPRIGHT);

            Rectangle botleft = RectangleToScreen(new Rectangle(0, Height - 8, 8, 8));

            if (botleft.Contains(p))
                return new IntPtr(HTBOTTOMLEFT);

            Rectangle botright = RectangleToScreen(new Rectangle(Width - 8, Height - 8, 8, 8));

            if (botright.Contains(p))
                return new IntPtr(HTBOTTOMRIGHT);

            Rectangle top = RectangleToScreen(new Rectangle(0, 0, Width, 8));

            if (top.Contains(p))
                return new IntPtr(HTTOP);

            Rectangle cap = RectangleToScreen(new Rectangle(0, 8, Width, 38 - 8));

            if (cap.Contains(p))
                return new IntPtr(HTCAPTION);

            Rectangle left = RectangleToScreen(new Rectangle(0, 0, 8, Height));

            if (left.Contains(p))
                return new IntPtr(HTLEFT);

            Rectangle right = RectangleToScreen(new Rectangle(Width - 8, 0, 8, Height));

            if (right.Contains(p))
                return new IntPtr(HTRIGHT);

            Rectangle bottom = RectangleToScreen(new Rectangle(0, Height - 8, Width, 8));

            if (bottom.Contains(p))
                return new IntPtr(HTBOTTOM);

            return new IntPtr(HTCLIENT);
        }
That will be used to figure out where the user clicked so you can do things such as re-size and move. At this point you can change the last line that says "return new IntPtr(HTCLIENT)" to "return new IntPtr(HTCAPTION)" and now have a panel like windows form you can drag around.

Now you can make a few images to mimic the client area and it will respond correctly. Here is a layout I created...

Note: When adding images, you must use the OnPaint method to draw them. If you use panels, they will get on top of the rectangles used to tell when the user is clicking on the "non client area" dedicated rectangles. This means that your re-size and move commands will be eliminated.

Image

There is a simple one I just made.

Okay, now for everyone running Windows 7 and Vista. There is a lot more that can be done with the fake non client area. You can have a non client area with glass, so you create the illusion of placing items on a non client area as opposed to creating a custom non client area.

We will use 2 external methods to do this. The first being DwmExtendFrameIntoClientArea(...) and the second being DwmDefWindowProc(...).

So above your constructor put these three methods:

Code: Select all

/// <summary>
/// Method used to tell if Destop Composition is enabled, or if Windows Aero is enabled.
/// </summary>
/// <returns></returns>
[DllImport("dwmapi.dll")]
private static extern int DwmIsCompositionEnabled(ref bool IsCompositionEnabled);
And:

Code: Select all

[DllImport("dwmapi.dll")]
private static extern int DwmDefWindowProc(IntPtr hwnd, int Message, IntPtr wParam, IntPtr lParam, out IntPtr LResult);
And last:

Code: Select all

[DllImport("dwmapi.dll", PreserveSig = false)]
private static extern void DwmExtendFrameIntoClientArea(IntPtr hwnd, ref Margins margins);
You will need a reference for DllImport...

Now we need our Margins structure to contain our data for the windows Aero glass extension.

Code: Select all

/// <summary>
/// A structure that controls the form's glass extension.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
private struct Margins
{
public int Left;
public int Right;
public int Top;
public int Bottom;
}
Now make a void to check if you have Desktop Composition enabled...

Code: Select all

public bool IsDesktopCompEnabled()
{
if (Environment.OSVersion.Version.Major < 6)
{
return false;
}

bool ReturnValue = false;
DwmIsCompositionEnabled(ref ReturnValue);
return ReturnValue;
}
That will check if you have Windows Aero enabled. Now lets go back to our WndProc method and cut all of the contents out, and keep them in your clipboard for a minute. Put into the method this:

Code: Select all

if (IsDesktopCompEnabled())
{
base.WndProc(ref m);
}
else
{
}
and paste your clipboard into the else snippet.

Now debug and if you run Windows Aero, you should see a standard form, if you are on basic, you will see that custom form shown before.

Okay, now for the fun stuff! Go to the HitTestNCA method and locate the rectangle which represents the caption bar. It will look like this:

Code: Select all

Rectangle cap = RectangleToScreen(new Rectangle(0, 8, Width, 38 - 8));
if (cap.Contains(p))
return new IntPtr(HTCAPTION);
Change it to:

Code: Select all

if (UseMargins)
{
Rectangle cap = RectangleToScreen(new Rectangle(0, 8, Width, MARGINS.Top - 8));
if (cap.Contains(p))
return new IntPtr(HTCAPTION);
}
else
{
Rectangle cap = RectangleToScreen(new Rectangle(0, 8, Width, 38 - 8));
if (cap.Contains(p))
return new IntPtr(HTCAPTION);
}
Now that will use two non-existant references: The first being UseMargins, and the second being MARGINS. Create a bool named UseMargins and set it to false defaultly. Now make a reference of Margins named MARGINS.

Now we will be ready to make some glass! Go back to your WndProc method and delete "base.WndProc(ref m);" because we won't need it right now.

Place this into the If snippet:

Code: Select all

IntPtr result;
int dwmHandled = DwmDefWindowProc(m.HWnd, m.Msg, m.WParam, m.LParam, out result);
if (dwmHandled == 1)
{
m.Result = result;
return;
}
We need that DwmDefineWindowProc to give life to our caption buttons. If you ignore the line of code that sets the result, you will have a close minimize and maximize button that do nothing.

Now place the NCCALCSIZE_PARAMS stuff below that.

Code: Select all

if (m.Msg == 0x0083 && (int)m.WParam == 1)
{
NCCALCSIZE_PARAMS nccsp = (NCCALCSIZE_PARAMS)Marshal.PtrToStructure(m.LParam, typeof(NCCALCSIZE_PARAMS));

// Adjust the client rectangle to create a new 'client area'
nccsp.rect0.Top += 0;
nccsp.rect0.Bottom += 0;
nccsp.rect0.Left += 0;
nccsp.rect0.Right += 0;
Now we need to set our margins up and add our result...

Code: Select all

if (!UseMargins)
{
//Create New Reference...
MARGINS = new Margins();
//Set what client area would be for passing to DwmExtendIntoClientArea
MARGINS.Top = nccsp.rect2.Top - nccsp.rect1.Top;
MARGINS.Left = nccsp.rect2.Left - nccsp.rect1.Left;
MARGINS.Bottom = nccsp.rect1.Bottom - nccsp.rect2.Bottom;
MARGINS.Right = nccsp.rect1.Right - nccsp.rect2.Right;
UseMargins = true;
}
Marshal.StructureToPtr(nccsp, m.LParam, false);
m.Result = IntPtr.Zero;
}
Than the else if...

Code: Select all

else if (m.Msg == 0x0084 && (int)m.Result == 0)
{
m.Result = HitTestNCA(m.HWnd, m.WParam, m.LParam);
}
else
{
base.WndProc(ref m);
}
Okay, now we need to extend our frame and draw the glass...

Now override the OnActivate method.

Code: Select all

protected override void OnActivated(EventArgs e)
{
if (IsDesktopCompEnabled() && UseMargins)
{
//Extend!
DwmExtendFrameIntoClientArea(this.Handle, ref MARGINS);
}
base.OnActivated(e);
}
That will ensure that you have all of the prerequisites to extending the frame into the client area...

Now you need to paint the region where the form border extends in so override the OnPaint method.

Add this code to the method body:

Code: Select all

if (IsDesktopCompEnabled() && UseMargins)
{
//Clear
e.Graphics.Clear(Color.Transparent);
//Fill Glass area...
e.Graphics.FillRectangle(new SolidBrush(BackColor),
Rectangle.FromLTRB(
MARGINS.Left - 0,
MARGINS.Top - 0,
Width - MARGINS.Right - 0,
Height - MARGINS.Bottom - 0));
//OnPaint
base.OnPaint(e);
}
else
{
base.OnPaint(e);
}
Now debug... It looks good right? No wrong try resizing it.

Image

Simple fix: First go to the constructor method body and enable DoubleBuffering.

Second: Override the OnResize method and tell it to refresh.

Now you can place items on the Glass region on your form!

You can change the size of the glass region by adjusting the rectangle size of the NCCALCSIZE_PARAMS.

So here is my final result:
Image

I hope that you learned from this tutorial. Please let me know whether you like this or not by leaving a comment.
Image
Post Reply