PDA

View Full Version : آموزش: فرم های گرافیکی



alireza_s_84
دوشنبه 24 اسفند 1388, 23:43 عصر
دوستان خوبم سلام:
خیلی وقت بود که اینجا نیومده بودم حالا بعد از این همه مدت(3 ماه) دست پر اومدم با یه عیدی واسه همه ی بر و بچه های برنامه نویس بخصوص اوناییکه دنبال فرمهای گرافیکی و ظاهر خوب واسه برنامه هاشون هستن.
میخوام آموزش ساخت یک فرم ویندوز با ظاهر دلخواه رو بهتون آموزش بدم میدونم که همه نیازمند این مطلب هستن.
مقدمه:
ساختار سیستم عامل ویندوز به گونه ای طراحی شده که برای انجام کارهای خود پیغامهایی رو پنجره ها در بین خود با سیستم عامل رد و بدل میکنند که این پیغامها هرکدوم مربوط به زمان خاص و عملی خاص هستند و هرکدوم اونها هم کد مخصوصی داره.
تمامی این پیغامها بصورت ثوابت (Const) تعریف شدن و کسانیکه با توابع API کار کردن این پیغامها رو بی شک استفاده کردن.
سالها پیش که VB6 کار میکردم برای خودم کلاسهایی ساخته بودم و اونها رو بصورت dll کامپایل کرده بودم که دقیقا سالها بعد متوجه شدم ای دل غافل ما هم کتابخانه و فریم ورک نوشتیم و خودمون خبر نداریم!!!
برای مثال کلاسی داشتم بنام Animation که کارش افکت دادن و جنگولک بازی روی کنترلها و فرمها بود یکسری توابع API رو فراخوانی میکرد و پارامترهایی از نوع Enum ارسال میکرد و خلاصه از صدا زدن توابع API و ارسال ثوابت خبری نبود .
تا اینکه با دات نت آشنا شدم و بعد با reflector ها نگاهی به سورس کتابخانه ها و کلاس کنترل انداختم و فهمیدم که کار این کتابخانه همون فراخونی API هاست و فقط کار رو راحتتر کرده واسه همینه که سرعت اجرای برنامه های دات نت پایینه چون این کتابخانه ابتدا کدها رو با مفسر خودش و بعد در IL و بعد در لایه ی بعدی (که اینجا میشه همون فراخونی API ها) اجرا میکنه و لذا سرعت اجراش از زبان VB6 پایینتره VB6 یک مرحله کمتر داره یعنی تبدیل به IL نمیشه ولی ++C مستقیم با همین dll ها سر و کار داره و لذا دیگه واسطه بازی و اینها نداره واسه همین سریعه.
(خودمونی مینویسم که دوستان تازه کار مثل خودم بهتر بفهمن اگر کسی دوست داشت میتونم خیلی علمی تر و پیچیده تر و با مدرک و سند باهاش بحث کنم)
خوب توابع API که چیزی حدود 7500 تابع هستن و با ثوابت و متغیرها و نوع ها و ... برایخودشون عالمی دارن همه و همه توی دو سه تا کلاس دات نت سرازیر شدن که مهمترین اونها سه کلاس زیر هستن:
SafeNativeMethods
UnsafeNativMethods
NativeMethods

چگونگی ایجاد کنترلها و پنجره ها در سیستم عامل ویندوز:
سیستم عامل ویندوز برای ایجاد کنترلهایی که ما میبینم و توی چند مورد خلاصه میشن (بقیه کنترلها مثل گریدها ترکیبی از این کنترلها هستن) از یک تابع API بنام CreateWindowEx استفاده میکنه که کار این تابع ایجاد یک پنجره یا کنترل است.
نگاهی دقیقتر به این تابع میندازیم:
نحوه تعریف VB .net

Declare Function CreateWindowEx Lib "user32" Alias "CreateWindowExA" (ByVal dwExStyle As Long, ByVal lpClassName As String, ByVal lpWindowName As String, ByVal dwStyle As Long, ByVal x As Long, ByVal y As Long, ByVal nWidth As Long, ByVal nHeight As Long, ByVal hWndParent As Long, ByVal hMenu As Long, ByVal hInstance As Long, lpParam As Any) As Long

نحوه تعریف C#‎‎‎‎‎‎

[DllImport("user32.dll", EntryPoint="CreateWindowEx", CharSet=CharSet.Auto, SetLastError=true)]
public static extern IntPtr IntCreateWindowEx(int dwExStyle, string lpszClassName, string lpszWindowName, int style, int x, int y, int width, int height, HandleRef hWndParent, HandleRef hMenu, HandleRef hInst, [MarshalAs(UnmanagedType.AsAny)] object pvParam);

من از توضیحات جزیی در مورد این تابع صرف نظر میکنم و انو به عهده شما میذارم فقط نکته مهم پارامتر dwStyle این تابع هست که استایل و شمایل پنجره یا کنترل ساخته شده رو تنظیم میکنه.
برای دیدن جزییات بیشتر در مورد استایل ها Extended Window Styles و Window Styles رو توی MSDN جستجو کنید.
محل استقرار این تابع گرامی و مهم و محترم در کلاس UnsafeNativeMethods قرار داره و اما چرا در مورد این تابع توضیح دادم ؟؟؟ برای اینکه تمام کار ما برای شناخت فرم های ویندوزی بر اساس این تابعه.
زیاد وارد جزییات نمیشم ولی کار ثوابت Class Styles تعیین شکل پنجره ای که قراره ساخته بشه برای مثال ثابت WS_CAPTION پنجره ای با TitleBar ایجاد میکنه که خوب معلومه میشه فرم دیگه.
نتیجه نهایی اینکه چرا تمامی کنترلهای دات نت از کلاس Control ارث میبرن رو هم حالا بهتر میشه فهمید چون تمامی کنترلها با یک تابع ساخته میشن و فقط شکلشون فرق میکنه و درون کلاس Control یک پارامتر وجود داره به نام CreateParams که کنترلهای مختلف اونو Override میکنن تا شمایلی مخصوص خودشون رو اعمال کنن.
این پارامتر همون کار پارامتر dwStyle انجام میده.

تابع DefWndProc :
این همون تابع معروفیه که در تمامی زبانهای برنامه نویسی ازش استفاده میشه تابعی که پیغامهای پنجره جاری رو به سیستم عامل انتقال میده.
برای مثال هنگامیکه یک پنجره نیاز به ترسیم داره از طریق این تابع پیغام WM_Paint رو به سیستم عامل میفرسته.

پیغامهایی که جهت ترسیم بکار میروند:
جهت اطلاعات بیشتر در مورد توابعی که تمامی ترسیمات رو در دات نت و سایر محیطهای برنامه نویسی انجام میدن Painting and Drawing Functions رو توی MSDN جستجو کنید.
و اما تمامی پیغامهای ترسیمی که سیستم عامل ویندوز از اونا استفاده میکنه :



WM_DISPLAYCHANGE
WM_ERASEBKGND
WM_ICONERASEBKGND
WM_NCPAINT
WM_PAINT
WM_PAINTICON
WM_PRINT
WM_PRINTCLIENT
WM_SETREDRAW
WM_SYNCPAINT

برای ایجاد فرمهای دلخواه ما کار اصلی رو با پیغام WM_NCPAINT انجام میدیم. این پیغام زمانی به سیستم عامل ارسال میشه که یک پنجره (فرم) باید Titlebar یا همون Caption رو ترسیم کنه.
با دونستن این نکته براحتی میتونیم با Intercept کردن این پیغام در تابع WndProc ترسیمات دلخواهمون رو بجای ویندوز اعمال کنیم.

alireza_s_84
جمعه 10 اردیبهشت 1389, 11:27 صبح
قسمت دوم آموزش فرمهای گرافیکی:
بعد یک ماه و چند روز امروز میخوام بقیه مطالب رو بگم هرچند تو این مدت استقبالی از تاپیک نشد ولی فکر میکنم به خاطر اون روزهایی بود که مسولین به سایت برنامه نویس لطف کردن و جز سایتهای ممنوعه شناخته شده بود و تا مشکلش برطرف شد این تاپیک هم به آرشیو پیوست.
رسیدیم به اونجا که با Intercept کردن پیغام WM_NCPAINT در رویداد WndProc فرم میتونیم فرمهامون رو از شکل و شمایل زشتی که دارن یعنی همون Caption آبی رنگ راحت کنیم.
برای ترسیم رو عونان فرم ما نیاز به توابع API داریم و چون هر کدوم این توابع پارامترهای خاص خودشون رو دارند و گاهی مواقع از ساختارها هم استفاده میکنند لذا من به مانند خود فریم ورک دات نت یک کلاس بنام NativeMethods ایجاد میکنیم که این کلاس برای نگهداری توابع API و پارامترها و ساختارهای مورد نیاز برای این توابع است. یکی دو تا تابع دات نتی که به اون اضافه خواهیم کرد ولی در کل این کلاس یک وظیفه بسیار مهم داره و اون بکارگیری روش خود فریم ورک برای استفاده از کتابخانه ویندوزی API است.
اگر خاطرتون باشه گفتم که فریم ورک چیزی جز همین توابع API نیست و تمام این توابع رو فقط مرتب کرده و اول و آخر ما با توابع API سر و کار داریم.
این کلاس نباید قابلیت اشتقاق داشته باشه و لذا اون رو از sealed تعریف میکنیم که معادل اون توی VB میشه NotInheritable .
این کلاس دو بخش تقسیم میشه:
الف) توابع API مورد نیاز برای ترسیمات روی فرم و یا گرفتن هندل و تخریب هندلهای گرفته شده.
ب) ثوابت ، ساختارها و پیامهایی که این توابع از اونها استفاده میکنند.

ترسیم Caption دلخواه بر روی فرم:
حال کار خودمون رو بر روی فرم انجام میدیم. اگر یادتون باشه گفتم که فرم برای ارسال پیغامهای خود به سیستم عامل از تابع DefWndProc استفاده میکنه و اینکار در رویداد WndProc فرم انجام میشه پس ابتدای کار با Override کردن این رویداد منطق خودمون رو پیاده سازی میکنیم:
protected override void WndProc(ref Message m)
{base.WndProc(ref m);}
در مورد پارامتر m که از نوع Message هست توضیحاتی میدم:
ساختار Message اشاره به ساختاری دارد که یک نوعی رو ایجاد میکنه این نوع شامل موارد زیره:
hWnd که یک اشاره گر(Handel) به پنجره یا کنترلی است که این پیغام رو ارسال کرده.
msg کد پیغامی که ارسال شده
wparam پارامتریی که حاوی اطلاعات اضافی در مورد پیغام صادر شده است (این اطلاعات میتونه مثلا شامل ناحیه ترسیمی(Region) روی کنترل یا پنجره باشه)
lparam پارامتریی که حاوی اطلاعات اضافی در مورد پیغام صادر شده است ولی این اطلاعات خیلی بزرگتر از پارامتر wparam هست.
برای شناخت بهتر این پارامترها اینجا رو یک نگاهی بندازید:
http://msdn.microsoft.com/en-us/library/system.windows.forms.message.wparam.aspx
http://msdn.microsoft.com/en-us/library/system.windows.forms.message.lparam.aspx

خوب کار عمده ای که ما انجام میدیم با همین پارامتر m و فیلدهای اون هست . ابتدا کار باید تمامی پیغامهای ارسالی ویندوز رو توی یک ساختار تعریف کنیم تا راحتتر باشیم.
هر پیغام ارسالی به سیستم عامل به این شکله:
WM_RBUTTONDOWN = 0x0204 یعنی پیغام هنگام کلیک راست موس پیام WM_RBUTTONDOWN با کد عددی 0x0204 از فرم شما به سیستم عامل توسط رویداد WndProc و اون بواسطه تابع DefWndProc انتقال داده میشه.
من اینکار رو توی کلاس NativeMethods انجام میدم برای اینکار یک نوع شمارشی Enum بنام WindowMessages ایجاد میکنم و تمام پیامهای ویندوز رو توش قرار میدم:
public enum WindowMessages
{
WM_ACTIVATE = 6,
WM_ACTIVATEAPP = 0x1c,
WM_CAPTURECHANGED = 0x215,
ادامه دارد .....
}
خوب از اونجا که تعداد پیغامهای ویندوزی زیاده من همه رو اینجا نمینویسم فقط هدف اینه که شما بدونید چی هستن.در مرحله بعد توی رویداد wndproc چک میکنیم هرگاه پیغام ارسالی WM_NCPAINT بود اون رو هندل کنیم:

protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case (int)NativeMethods.WindowMessages.WM_NCPAINT:
{
break;
}
default:
{
base.WndProc(ref m);
break;
}
}



خوب اگر پیغام WM_NCPAINT بود عملیات خودمون رو برای ترسیمات انجام میدیم و گرنه بهمون حالت اونو به رویداد WndProc کلاس والد انتقال میدیم. یعنی(base.WndProc(ref m))

برای امروز کافیه بقیه مطالب رو میذارم برای یک فرصت مناسب.

alireza_s_84
پنج شنبه 23 اردیبهشت 1389, 20:16 عصر
بخش سوم آموزش رو ادامه میدم:
اول از همه باید مشخص کنیم که چکار میخوایم بکنیم. یعنی فرمی که میخوایم درست کنیم چه شکلی باشه . من علاقه خاصی به فرمهای یاهو مسنجر دارم کلی اسکین و اسکینر دیدم ولی هیچکدوم به اندازه فرم های یاهو به من نچسبیده بخصوص تم رویالش.
قصدم هم همینه که فرمی که درست میکنم دقیقا مثل فرمهای یاهو باشه با اسکین های مختلف در نهایت آموزش ما فرمهایی به شکل زیر خواهیم داشت:


http://i42.tinypic.com/25aopap.png

توجه داشته باشید که ما برای درست کردن این فرم هیچکدوم از روشهای زیر رو استفاده نخواهیم کرد:
1) استفاده از فرمهای بدون Border.
2) استفاده از یک عکس برای زمینه و تنظیم خاصیت TransparencyKey
بلکه ما دقیقا همانند یک فرم عملیات پاینت و ترسیم رو انجام میدیم و لذا فرمی که بدست میاد تمامی استانداردهای یک فرم معمولی داره.
ابتدای امر ما نیاز به فایلهای عکس جهت پوسته های مختلف داریم. من این عکس ها رو از پوشه تم یاهو برداشتم و عکس های مربوط به دکمه های Close&Resize&Minimize رو هم از به تقلید از ویندز سون تهیه کردم.
این مرحله رو با ایجاد فایل های Resource شروع میکنم ابتدا فایل زیپ عکس ها رو قرار میدم این فایلها پوسته های فرم ما خواهد بود و بر خلاف یاهو مسنجر ما این عکس ها رو با خود فایل exe کامپایل میکنیم چون در این مرحله تنها هدف ما چگونگی ایجاد اینگونه فرمهاست و حتما در آموزشهای بعدی چگونگی ایجاد پوسته های خارجی رو هم شرح میدم. نهایتا هدف من چگونگی ایجاد یک Skinner یا موتور اعمال پوسته است. ولی چون ایجاد درک مناسب بدون دونستن مفاهیم اولیه امکانپذیر نیست لذا تمامی این مراحل رو باید خوب درک کرد تا به اون سطح برسیم.
بعد از دانلود عکس ها یک پروژه از نوع Class Library ایجاد کنید و این عکس ها رو به Resource اضافه کنید.
بعد از اینکار یک کلاس به پروژه تون اضافه کنید و اسم اون رو NativeMethods بذارید در مرحله قبل کار این کلاس و توضیحات تکمیلی داده شد.
من کلاس NativeMethods رو برای دانلود قرار میدم که شما زحمت کدنویسی رو نکشید ولی اگر از این آموزش واقعا به دنبال یادگیری هستید یکبار سعی کنید دقیق بهش نگاهی بندازید و بفهمید چه توابع API رو وارد کرده و چه ثوابت و ساختارهای رو استفاده نموده تا بعدها در حین آموزش ساخت موتور پوسته ساز (Skinner) تاپیک رو با سوالات اضافی شلوغ نکنید چون بی شک عدم شناخت این مرحله منجر به پرسیدن سوالات بسیاری میشه که جوابشون در این کپی برداری هاست.( حداقل خوب نگاه کنید نکنه من توی این کلاس کاری کرده باشم که بعد یه مدت فرم از کار بیفته:متفکر:)
خوب میریم سر اصل مطلب بعد از دانلود کلاس NativeMethods اونو به پروژه تون اضافه کنید در حال حاضر تنها از 5 الی 6 متد موجود در این کلاس استفاده میشه ولی بعدها به سایر متدها نیاز داریم پس از حذف متدهای اضافی خودداری کنید اگر هم دوست داشتید حذف کنید اختیار با شماست.
بعد از این مرحله نوبت ایجاد کلاس فرم ماست برای اینکار یک فرم به پروژه تون اضافه کنید یا اینکه یک کلاس ایجاد کنید و از کلاس Form ارث ببرید فرقی نمیکنه اسم این کلاس رو هم من YahooForm گذاشتم.
کار رو با Intercept کردن پیامهای ارسالی فرم شروع می کنیم:
پیغام WM_NCPAINT: این پیام هنگامی به فرم ارسال میشه که فرم یا پنجره نیاز به ترسیم Caption خود داره و معروف به ناحیه غیر کاربری است.
WM_NCPAINT مخفف » M پیام PAINT ترسیم NC ناحیه غیر کاربری W پنجره است.

protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case (int)NativeMethods.WindowMessages.WM_NCPAINT:
{
WmNCPaint(ref m);
break;
}

default:
{
base.WndProc(ref m);
break;
}
}
}

ابتدا چک میکنیم اگر پیام ارسالی پیام WM_NCPAINT بود اون رو بصورت یک پارامتر با مراجعه به متد WmNCPaint پاس میکنیم در غیر اینصورت پیام رو به همون صورتی که هست به متد WndProc کلاس پایه پاس میدیم.
متد WM_NCPAINT کار ترسیمات Caption فرم رو انجام میده:


private void WmNCPaint(ref Message msg)
{
PaintNonClientArea(msg.HWnd, msg.WParam);
msg.Result = NativeMethods.TRUE;
}


در بدنه متد ابتدا متدی با نام PaintNonClientArea فراخونی میشه که دو تا پارامتر میگیره:
1) یک هندل از شی جاری که میشه همین کلاس که در واقع فرم ماست
2) یک پارامتر از نوع IntPtr که Region یا ناحیه فرم ما رو تعیین میکنه.
ناحیه یا IntPtr عبارت است از دور تا دور یک پنجره یا ناحیه ای که پنجره ما اشغال میکنه مثل محیط حال اگه فرم ما به شکل دایره بود ناحیه ای بصورت دایره و اگر مستطیل بود ناحیه ای بصورت مستطیل اشغال میکنه.
Region فرم درون خاصیت WParam شی Message قرار داره (توضیحات تکمیلی در دو قسمت قبلی داده شده)
پس با پاس کردن این دو پارامتر نوبت به این میرسه که به سیستم عامل بفهمونیم که این پیام هندل شده و تو دیگه رسیدگی نکن( msg.Result = NativeMethods.TRUE)
این قسمت دقیقا همون کاری رو میکنه که شما موقع بستن یک فرم خاصیت e.Cancel رو برابر True|False قرار میدین. یعنی در واقع هنگامیکه شما در بستن فرم اینگونه عمل میکنید در پشت صحنه چارچوب دات نت این کاریی که ما در کد بالا انجام دادیم رو انجام میده.

private void PaintNonClientArea(IntPtr hWnd, IntPtr hRgn)
{
NativeMethods.RECT windowRect = new NativeMethods.RECT();
if (NativeMethods.GetWindowRect(hWnd, ref windowRect) == 0)
return;

Rectangle bounds = new Rectangle(0, 0,
windowRect.right - windowRect.left,
windowRect.bottom - windowRect.top);

if (bounds.Width == 0 || bounds.Height == 0)
return;

Region clipRegion = null;
if (hRgn != (IntPtr)1) clipRegion = System.Drawing.Region.FromHrgn(hRgn);

IntPtr hDC = NativeMethods.GetDCEx(hWnd, hRgn,
(int)(NativeMethods.DCX.DCX_WINDOW | NativeMethods.DCX.DCX_INTERSECTRGN
| NativeMethods.DCX.DCX_CACHE | NativeMethods.DCX.DCX_CLIPSIBLINGS));

if (hDC == IntPtr.Zero) return;
try
{
using (Graphics g = Graphics.FromHdc(hDC))
{
g.SetClip(bounds);
OnNonClientAreaPaint(new PaintEventArgs(g, bounds));
}
}
finally
{
NativeMethods.ReleaseDC(this.Handle, hDC);
}
}


خط به خط متد رو شرح میدم:
ابتدا یک متغیر از نوع RECT ایجاد میکنیم . ساختار RECT اشاره به یک مستطیل داره که 4 گوشه اون محل قرار گیری فرم ماست حال فرم چه داریه باشه چه به شکل نامرتبی مثل گل و یا مستطیل باشه.
در خط بعد با استفاده از متد GetWindowRect کلاس NativeMethods و پاس دادن متغیر تعریف شده بصورت با مراجعه (برای اینکه این متد کار پر کردن خصوصیات ساختار RECT رو برای ما انجام میده) ساختار RECT تعریف شده رو با مختصات فرم پر میکنیم.
در صورتیکه این تابع موفق به انجام کار نشه و خطایی رخ بده مقدار صفر بر میگردونه(میتونید این تابع API رو سرچ کنید و اطلاعات لازم در مورد نحوه کار اون رو بفهمید)
سپس یک متغیر از نوع Rectangle برای نگهداری محل قرارگیری فرم و ابعاد اون تعریف میکنیم.
اگر طول یا عرض این Rectangle تعریف شده 0 باشه هیچ کاری انجام نمیدم و از متد خارج میشم.
در مرحله بعد با استفاده از پارامتر hrgn باید Region فرم رو بدست بیاریم تا بتونیم با گرفتن یک دستگیره از ساختار پنجره بر روی اون ترسیم کنیم.
اینکار با تابع FromHrgn از کلاس Region انجام میشه اونهم به شرطی که rgn به دست اومده از خاصیت Wparam شی Message برابر 1- نباشه.
در خط بعد با تغییر متغری بنام hDC از نوع IntPtr برای نگهداری یک هندل از ساختار فرم کار رو ادامه میدیم.
تابع API GetDCEx کلاس NativeMethods با گرفتن پارامترهای لازمه یک هندل یا HDC به ما بر میگردونه.
در مورد HDC و تابع GetDCEx خودتون جستجو کنید چون توضیحات جامعی نیاز داره ولی در مورد پارامتر آخر من در بخش یک آموزش توضیحات لازمه رو دادم.
فقط یاداوری میکنم که با این کار ما یک ناحیه رو برای ترسیم علامتگذاری میکنیم.
اگر hDC بدست اومده 0 باشه یعنی فرم ما نیاز به ترسیم نداره (ممکنه Minimize باشه) پس از متد خارج میشیم.
ما نیازمند یک بلوک try finally چون در صورت وقوع خطا حافظه رزور شده برای HDC هیچوقت باز نمیگرده و این یعنی کاهش منابع سیستم پس حتما باید حافظه گرفته شده برای یک HDC به سیستم عامل برگرده.
یک شی Graphics با استفاده از HDC بدست اومده ایجاد میکنیم و به همراه شی Rectangle مختصات و ابعاد فرم به متد OnNonClientAreaPaint ارسال میکنیم. کار ترسیمات اصلی رو اینجا انجام میدیم.
این متد یک پارامتر از نوع PaintEventArgs میگیره که ما با نمونه سازی از این کلاس با استفاده از شی گرافیک بدست اومده از HDC و ساختار مشخصات و مختصات فرم متد OnNonClientAreaPaint رو فراخونی میکنیم برای درک بهتر رویداد Paint یک فرم رو در نظر بگیرید در واقع ما تمامی این کارها رو کردیم تا بتونیم رویدادی مثل رویداد Paint فرم ایجاد کنیم.
دقیقا چارچوب دات نت همین رویه رو برای یک فرم استفاده میکنه . ممکنه سوالی پیش بیاد که چرا باید اینکار رو بکنیم مگر نمیشه تو همون رویداد Paint ترسیمات رو انجام داد؟
پاسخ منفیه چون رویداد Paint تنها به ما اجازه ترسیم بر روی ناحیه کاربی رو میده یعنی ناحیه ی بین caption و بردرها.
ما برای اینکه بتونیم روی caption ترسیم کنیم نیازمند مرحله فوق هستیم.
البته اگر شما نیاز دارید تا ابعاد ناحیه غری کاربری(caption) رو تنظیم کنید باید پیام WM_NCCALCSIZE رو هندل کنید ولی چون ما ابعاد ناحیه غیر کاربری رو تغییر ندادیم لذا نیازی به اینکار نیست.
اگر قرار باشه شما مانند فرم آفیس یک دایره توی گوشه سمت چپ نشون بدید باید پیام WM_NCCALCSIZE رو هندل کنید و سایز جدید رو برای فرم تعریف کنید. این قسمت شامل این آموزش نمیشه ولی در مرحله بعد که نحوه ساخت یک فرم آفیس هست حتما این مطلب رو آموزش میدم.
فکر میکنم برای این جلسه هم کافی باشه بقیه آموزش میمونه برای جلسه بعدی.

alireza_s_84
جمعه 24 اردیبهشت 1389, 10:53 صبح
بخش چهارم آموزش:

برای شروع عملیات اصلی ترسیم بر روی عنوان فرم ابتدا یک تابع رو بصورت زیر تعریف می کنیم:

private void DrawButton(Graphics g, Image img)
{
if (this.ControlBox == false)
return;

g.DrawImage(img, btnRect);
}

ابتدای امر باید بگم کار این تابع ترسیم دکمه های Close & Min & Resize کردن فرم هست. شما باید فیلدهای خصوصی زیر رو برای فرم تعریف کنید:

private Rectangle btnRect;
private Rectangle CloseRect;
private Rectangle MaxRect;
private Rectangle ResRect;
private Rectangle IconRect;

به ترتیب:
محل قرار گیری دکمه های عنوان که همون Close & Min & Resize هستند.
محل قرارگیری دکمه Close
محل قرار گیری دکمه Minimize
محل قرار گیری دکمه Resize
محل قرار گیری Icon
این فیلدها رو چه زمانی مقدار دهی میکنیم؟ زمانیکه ابعاد فرم تغییر بکنه یعنی در رویداد OnResize فرم.
از اونجا که ما با تغییر خاصیت RTL فرم قصد نداریم تا آیکون و عنوان فرم راست به چپ بشن (برای من مانوس نیست اینکار رو بکنم دوست دارم فقط ناحیه کاربری و کنترلها راست به چپ بشن نه دکمه ها و آیکون و عنوان فرم) پس با تغییر این خصوصیت تاثیری در مقدار دهی این فیلدها بوجود نمیاد اگر شما قصد دارید تا اینکار رو بکنید میتونید با گذاشتن یک شرط وضعیت رو چک کنید و این فیلدها رو که همه از نوع Rectangle هستند رو مقدار دهی کنید.


private bool CloseIsOver = false;
private bool MinIsOver = false;
private bool MaxIsOver = false;


سه فیلد زیر رو هم برای نگهداری وضعیت موس اور دکمه های نوار عنوان ایجاد کنید از همون ابتدا مقدار همه false هست چون فعلا موس ما روی این دکمه ها قرار نداره.

برای مشخص کردن نوع پوسته ای که باید به فرم اعمال بشه یک نوع شمارشی جدید در فضای نام پروژه منظور همون NameSpace بصورت زیر ایجاد کنید:

public enum FormSkin
{
Black,
Graffity,
Green,
Purple,
Royale,
RubyRed,
Silver,
SkyBlue,
TwinklePink,
VioletFlame,
Wood
}

پس با این حساب فرم ما دارای 11 پوسته خواهد بود.
فیلدهای زیر رو هم تعریف کنید:

private Font _CaptionFont = new Font("2 Titr", 11, FontStyle.Bold, GraphicsUnit.Point);
private Color _CaptionTextColor = Color.White;
private Boolean _CaptionTextShadow = true;
private Color _CaptionTextShadowColor = Color.Navy;
private FormSkin _FormSkin = FormSkin.Royale;

به ترتیب توضیح میدم:
فونت متن عنوان فرم که من تیتر انتخاب کردم.
رنگ متن عنوان فرم که من سفید انتخاب کردم(عکس بخش سوم آموزش رو ببینید)
تعیین اینکه متن عنوان فرم سایه داشته باشه یا نه که اگر داشته باشه قشنگتره.
رنگ سایه متن عنوان فرم ما.
نوع پوسته فرم که بطور پیش فرض رویال گذاشتم (عکس مربوط به بخش سوم آموزش)

حالا برای تغییر هر کدوم از اینها فیلدهای عمومی که همون خصوصیات Property فرم ما هستند رو تعریف میکنیم:

#region PublicFields
/// <summary>
/// فونت متن عنوان فرم
/// </summary>
[Category("Appearance"), Description("تعیین فونت متن عنوان فرم")]
public Font CaptionFont
{
get { return _CaptionFont; }
set { _CaptionFont = value; this.Refresh(); }
}
/// <summary>
/// رنگ متن عنوان فرم
/// </summary>
[Category("Appearance"), Description("تعیین رنگ متن عنوان فرم")]
public Color CaptionTextColor
{
get { return _CaptionTextColor; }
set { _CaptionTextColor = value; this.Refresh(); }
}
/// <summary>
/// سایه دار بودن متن عنوان فرم
/// </summary>
[Category("Appearance"), Description("تعیین سایه دار بودن متن عنوان فرم"), DefaultValue(true)]
public Boolean CaptionTextShadow
{
get { return _CaptionTextShadow; }
set { _CaptionTextShadow = value; this.Refresh(); }
}
/// <summary>
/// رنگ سایه متن عنوان فرم
/// </summary>
[Category("Appearance"), Description("تعیین رنگ سایه متن عنوان فرم")]
public Color CaptionTextShadowColor
{
get { return _CaptionTextShadowColor; }
set { _CaptionTextShadowColor = value; this.Refresh(); }
}
[Description("تعیین پوسته فرم"), Category("Appearance")]
public FormSkin FormSkin
{
get { return _FormSkin; }
set { _FormSkin = value; Invalidate(true); }
}
#endregion


خب اینها تمامی موارد لازم برای انجام ترسیمات بود.
تابع DrawButton دو تا Overloads داره که من فعلا یکیشو توضیح دادم یادتون باشه تا بعدا دومیشو هم توضیح بدم. همنوطور که گفتم کار این تابع ترسیم دکمه های نوار عنوان فرم ماست.
تابع زیر رو هم تعریف کنید . کار این تابع تعیین عکس دکمه ای است که باید ترسیم بشه. چک میکنه ببینه موس روی کدوم دکمه قرار داره تا عکس مربوطه رو از سورس استخراج کنه:

private Image GetCaptionImage()
{
Image imgResult = Resources.N;

#region Both
if (this.MaximizeBox == true && this.MinimizeBox == true)
{
if (this.WindowState == FormWindowState.Maximized)
{
if (CloseIsOver) imgResult = Resources.TCO;
else if (MaxIsOver) imgResult = Resources.TMO;
else if (MinIsOver) imgResult = Resources.TRO;
else imgResult = Resources.TN;
}
else
{
if (CloseIsOver) imgResult = Resources.CO;
else if (MaxIsOver) imgResult = Resources.MO;
else if (MinIsOver) imgResult = Resources.RO;
else imgResult = Resources.N;
}
}
#endregion

#region OnlyMax
else if (this.MaximizeBox == true && MinimizeBox == false)
{
if (this.WindowState == FormWindowState.Maximized)
{
if (CloseIsOver) imgResult = Resources.TRDCO;
else if (MaxIsOver) imgResult = Resources.TRDMO;
else imgResult = Resources.TRD;
}
else
{
if (CloseIsOver) imgResult = Resources.RDCO;
else if (MaxIsOver) imgResult = Resources.RDMO;
else imgResult = Resources.RD;
}
}
#endregion

#region OnlyMin
else if (this.MaximizeBox == false && MinimizeBox == true)
{
if (this.WindowState == FormWindowState.Maximized)
{
if (CloseIsOver) imgResult = Resources.TMDCO;
else if (MinIsOver) imgResult = Resources.TMDRO;
else imgResult = Resources.TMD;
}
else
{
if (CloseIsOver) imgResult = Resources.MDCO;
else if (MinIsOver) imgResult = Resources.MDRO;
else imgResult = Resources.MD;
}
}
#endregion

#region OnlyClose
else
{
if (CloseIsOver)
imgResult = Resources.SCO;
else
imgResult = Resources.SC;
}
#endregion

return imgResult;
}


فکر نمیکنم نیازی به توضیح داشته باشه خیلی واضحه.
تابع UpdateCaptiobBoxRec رو برای بروزرسانی محل قرارگیری دکمه های نوار عنوان ایجاد کنید:

private void UpdateCaptiobBoxRec()
{
btnRect = new Rectangle(this.Width - 100, 4, 93, 18);
CloseRect = new Rectangle(btnRect.X + 50, 4, 43, 18);
MaxRect = new Rectangle(btnRect.X + 25, 4, 25, 18);
ResRect = new Rectangle(btnRect.X, 4, 25, 18);
IconRect = new Rectangle(5, 5, 18, 18);
}

این هم که نیازی به توضیح نداره محل قرارگیری دکمها و متن و آیکون رو ست میکنه.
حالا رویداد OnResize رو Override کنید تا مختصات محل قرارگیری دکمه ها رو بروز کنید:

protected override void OnResize(EventArgs e)
{
base.OnResize(e);
UpdateCaptiobBoxRec();
CloseIsOver = false;
MinIsOver = false;
MaxIsOver = false;
Refresh();
}

این هم که خیلی واضحه و نیازی به توضیح نداره. هروقت فرم تغییر سایز داد این رویداد سبب برورسانی موقعیت دکمه ها میشه.
یک فیلد خصوصی برای فرم تعریف کنید که کار اون نگهداری وضعیت فرم ماست که ایا فعال هست یا نه. یعنی زمانیکه فرم فعال نیست و فوکوس رو در اختیار نداره رنگ نوار عنوان کمرنگ میشه حالا ما میخوایم همین کار رو پیاده سازی کنیم:

private bool appActive = false;

و اما آخرین تابعی که در ترسیمات دخالت داره استخراج عکسی هست که برای پوسته جاری باید بر روی نوار عنوان فرم ترسیم بشه یعنی استخراج عکسهای پوسته فرم:

private Image GetImageFromSkin()
{
if (appActive == false)
return (Image)Resources.ResourceManager.GetObject("dis" + this.FormSkin.ToString());
else
return (Image)Resources.ResourceManager.GetObject(this.Fo rmSkin.ToString());
}

کار این تابع اینه که با توجه به نام پوسته جاری عکس مربوطه رو از ریسورس برای استخراج میکنه با یک شرط کوچک فعال یا غیر فعال بودن فرم رو چک میکنه و عکس لازم رو استخراج میکنه.
در نهایت بر میگردیم به رویداد OnNonClientAreaPaint و کار ترسیمات رو انجام میدیم:

Image ImageSkin = GetImageFromSkin();
Rectangle Rec = e.ClipRectangle;

Region clipRegion = new Region(e.ClipRectangle);
clipRegion.Exclude(new Rectangle(Rec.Left + 5, Rec.Top + 30, Rec.Width - 10, Rec.Height - 34));
e.Graphics.Clip = clipRegion;

خط به خط توضیح میدم:
ابتدا عکس پوسته جاری استخراج میشه تابع مربوطه شرح داده شد.
یک متغیر از نوع Rectangle برای نگهداری مختصات و ابعاد فرم ایجاد میکنیم در واقع میخوایم به جای استفاده از e.ClipRectangle که یه مقدار طولانی هست از Rec استقاده کنیم.
سپس تعیین ناحیه ترسیمات ماست که میشه طول و عرض و مختصات برداری فرم.
در خط بعد ما حدود ترسیمات گرافیکی رو تعیین میکنیم به این صورت که ارتفاع نوار عنوان ما میشه 30 پیکسل و بردرهای ما هم هرکدام 5 پیکسل یعنی در واقع ما ناحیه ترسیم رو فقط ناحیه غیر کاربری در نظر میگیریم و جاییکه کنترلهای ما قرار میگیرن رو کاری نداریم.
در خط بعد این ناحیه کاربری رو به شی گرافیک اعمال میکنیم. شما ی گرافیک رو مانند یک بوم نقاشی در نظر بگیرید ابتدا از هر طرف یه حاشیه برای بوم تعیین میکنیم بعد یک قسمت از وسط بوم رو هم یک کاغذ میچسبونیم تا رنگ روی اون نیوفته و کثیف نشه بعد دور تا دور رو رنگ آمیزی یا نقاشی میکنیم.

#region CaptionBar
Rectangle CaptionRight = new Rectangle((Rec.Width - 60), 0, 60, 30);
e.Graphics.DrawImage(ImageSkin, CaptionRight, (ImageSkin.Width - 60), 0, 60, 28, GraphicsUnit.Pixel);

Rectangle CaptionMiddle = new Rectangle(115, 0, (Rec.Width - 175), 30);
e.Graphics.DrawImage(ImageSkin, CaptionMiddle, 110, 0, 5, 28, GraphicsUnit.Pixel);

Rectangle CaptionLeft = new Rectangle(0, 0, 115, 30);
e.Graphics.DrawImage(ImageSkin, CaptionLeft, 0, 0, 110, 28, GraphicsUnit.Pixel);
#endregion

#region Border
Rectangle BodyRight = new Rectangle(Rec.Width - 60, 30, 60, (Rec.Height - 33));
e.Graphics.DrawImage(ImageSkin, BodyRight, (ImageSkin.Width - 60), 38, 60, 5, GraphicsUnit.Pixel);
Rectangle BodyLeft = new Rectangle(0, 30, 110, (Rec.Height - 33));
e.Graphics.DrawImage(ImageSkin, BodyLeft, 0, 28, 110, 5, GraphicsUnit.Pixel);
#endregion

#region Buttom
Rectangle ButtomRight = new Rectangle((Rec.Width - 60), (Rec.Height - 5), 60, 5);
Rectangle ButtomMiddle = new Rectangle(105, (Rec.Height - 5), (Rec.Width - 165), 5);
Rectangle ButtomLeft = new Rectangle(0, (Rec.Height - 5), 105, 5);
e.Graphics.DrawImage(ImageSkin, ButtomRight, (ImageSkin.Width - 60), (ImageSkin.Height - 5), 60, 5, GraphicsUnit.Pixel);
e.Graphics.DrawImage(ImageSkin, ButtomMiddle, 130, (ImageSkin.Height - 5), 5, 5, GraphicsUnit.Pixel);
e.Graphics.DrawImage(ImageSkin, ButtomLeft, 0, (ImageSkin.Height - 5), 105, 5, GraphicsUnit.Pixel);
#endregion


این مرحله هم ترسیم عنوان فرم با استفاده از متد DrawImage شی گرافیک و همچنین ترسیم حاشیه های فرم ماست.
فکر نمیکنم نیازی به توضیح باشه فقط اشاره کنم که ابتدا محدوده یا قسمتی از عکس پوسته کا باید ترسیم بشه رو مشخص میکنیم بعد با استفاده از متد DrawImage شی گرافیک اون رو بر روی فرم ترسیم میکنیم.
در قسمت زیر نوبت به ترسیم متن نوار عنوان میشه:

#region PaintCaptionText
if (!String.IsNullOrEmpty(this.Text))
{
using (StringFormat SF = new StringFormat())
{
SF.Trimming = StringTrimming.EllipsisCharacter;
SF.FormatFlags = StringFormatFlags.NoWrap;
SF.LineAlignment = StringAlignment.Center;

Rectangle textRect = new Rectangle(24, 5, this.Width - 120, 18);
if (this.ShowIcon == false) textRect = new Rectangle(5, 5, this.Width - 120, 18);

if (this.CaptionTextShadow == true)
{
Rectangle shadowRect = textRect;
shadowRect.Offset(1, 1);
using (Brush b = new SolidBrush(this.CaptionTextShadowColor))
{
e.Graphics.DrawString(this.Text, this.CaptionFont, b, shadowRect, SF);
}
}
using (Brush b = new SolidBrush(this.CaptionTextColor))
{
e.Graphics.DrawString(this.Text, this.CaptionFont, b, textRect, SF);
}
}
}
#endregion

این رو هم خط به خط توضیح میدم:
چک میکنیم که متن نوار عنوان خالی یای پوچ نباشه.
یک متغیر از نوع StringFormat تعریف میکنیم که کار اون فرمت بندی متن ترسیمی ماست. این فرمت بندی شامل راست به چپ بودن تراز و نحوه قرارگیری متن ماست. میتونید این کلاس رو توی MSDN جستجو کنید.
ابتدا خصوصیت فرمت بندی رو تعیین میکنیم که اگر متن ما بیش از عرض فرم بود بصورت ... ادامه متن دیده بشه.
خصوصیت بعدی اینه که متن ما اگر بزرگ بود به دو خط نشکنه
خصوصیت بعدی هم تراز متن رو وسط قرار میدیم.
متغیر textRect برای ما محل قرارگیری متن رو نگهداری میکنه این مکان از بعد از آیکون فرم شروع میشه و تا قبا از دکمه های نوار عنوان تموم میشه و ارتفاع اون به اندازه ارتفاع نوار عنوان فرمه با کسر 5 پیکسل از بالا و پایین برای اینکه با لبه های نوار عنوان فیت نشه.
خطوط بعدی هم ترسیم متن نوار عنوان فرم با استفاده از متد DrawString شی گرافیک ماست.
در مرحله بعد نوبت به ترسیم آیکون فرم ماست:

if (this.ShowIcon == true && this.Icon != null)
{
Icon ico = new Icon(this.Icon, new Size(18, 18));
e.Graphics.DrawIcon(ico, IconRect);
}

این هم که اصلا توضیحی نداره فقط یادآوری کنم IconRect فیلد خصوصی بود و در قسمت های قبلی توضیح دادم که کجا مقدار دهی میشه. از اونجا که قرار شد خاصیت RTL فرم تاثیری روی مکان آیکون و دکمه ها نوار عنوان نداشته باشه دیگه کار اضافی انجام ندادیم و گرنه حتما باید با یک شرط محل ترسیم رو تعیین میکردیم.
و در نهایت ترسیم دکمه های نوار عنوان فرمه:

if (this.ControlBox == true)
{
Image btnCaption = GetCaptionImage();
DrawButton(e.Graphics, btnCaption);
}
GC.Collect();


ابتدا عکسی که باید برای دکمه ها ترسیم بشه رو استخراج میکنیم تابع مربوطه رو در بالا شرح دادم.
بعد دکمه ها ترسیم میشه که اون هم شرح داده شد.
در نهایت با فراخونی کلاس Garbage Colet تمامی حافظه های اشغال شده رو آزاد میکنیم. شما تی MSDN میتونید در این مورد بیشتر بدونید توضیح اون مفصله.
تا اینجای کار ما کار اصلی که ترسیم یک فرم گرافیکی بود رو تموم کردیم فقط مراحل دیگه ای مونده مثل هندل کردن رویدادهایی که روی دکمه های نوار عنوان صورت میگیره. تعیین وضعیت دکمه های نوار عنوان ، غیر فعال کردن پوسته ای که سیستم عامل به فرم اعمال میکنه و چند مورد دیگه که در بخش های بعدی آموزش شرح میدم.