جلسه ی نوزدهم (بررسی چند مثال از برنامه نویسی شی گرا)
مثال 1: سیستم پنجره ای:
اهداف مثال:
1.آشنایی با نحوه ی حل مساله و پیاده سازی کلاس ها ی لازم.
2.آشنایی عملی با مفهوم کلاس و اشیاء
جلسه ی قبل گفتیم که سیستم پنجره ای یعنی چه اگر به یاد ندارید حتما همین الان انتهای جلسه هجدهم را بخوانید.
اهداف کلاس:
گفتیم که در یک چنین سیستمی باید در هر لحظه
1.به پنجره ی فعال دسترسی داشته باشیم.
2.تعداد پنجره های موجود را بدانیم.
3.بتوانیم پنجره های دیگر را فعال کنیم(بین پنجره ها حرکت کنیم)
اهداف اشیاء نمونه سازی شده از کلاس(هر پنجره):
لازمه ی اینها این است که
4.هر پنجره بتواند خودش را رسم کند(ایجاد نمود گرا فیکی)
5. ویژگی های اساسی خود را مانند موقعیت قرار گیری در صفحه نمایش(left,top) , طول و عرض و رنگبندی را بداند.
6.بداند آیا پنجره ی فعال خودش است یا خیر.
و احتمالا یک سری امکانات اضاف که مایلیم هر یک از پنجره هایمان داشته باشند مانند عنوان یا محتوای متنی.
دید کلی:
یک کلاس به نام Window ایجاد می کنیم هر پنجره از این کلاس نمونه سازی می شود. این کلاس از طریق فیلد ها و توابع static به کنترل پنجره ها یعنی انتخاب پنجره فعال و دسترسی به آن و همچنین تعداد پنجره های موجود و... می پردازد.(موارد 1تا3) علت پیاده سازی توابع وفیلد های این بخش این است که می خواهیم
1.کار بر بدون نیاز به وجود اشیاء از این کلاس و از طریق نام کلاس به فیلد ها و توابع مذکور دسترسی داشته باشد.
2.اساسا نیازی نیست یک کپی از فیلدی که مثلا قرار است تعداد پنجره ها را ذخیره کند در هر شیء پنجره وجود داشته باشد.
3.این اعضا باید توسط تمام اشیاء این کلاس قابل دسترسی باشند و در واقع مشترک باشند.
4.اعضا استاتیک دقیقا دارای این ویژگی ها هستند.
برای دیترسی به پنجره ی فعال یک فلید استاتیک به نام activeWindow ایجاد می کنیم این فیلد از نوع اشاره گر خود کلاس Window است.
class Window{
static int activeIndex;
static Window** allWindows;
static Window* activeWindow;
static int windowsCount;
این فیلد اشاره گر است چون نمی خواهیم یک کپی از پنجره ی فعال در این فیلد داشته باشیم بلکه می خواهیم به خود پنجره ی فعال اشاره کند. چرا که می خواهیم از طریق این فیلد بتوانیم تغییراتی در پنجره ی فعال ایجاد کنیم بدون آنکه بدانیم پنجره ی فعال کدام پنجره است. اگر این فیلد اشاره گر نبود هنگام انتساب پنجره فعال به این فیلد یک کپی از آن در این فیلد ایجاد می شد در نتیجه با هر دسترسی به این فیلد بجای دسترسی به پنجره ی فعال به کپی آن دسترسی صورت می گرفت.
همچنین یک تابع عضو استاتیک به نام SelectNext() تعریف می کنیم وظیفه ی این تابع انتخاب پنجره ی بعدی یه عنوان پنجره ی فعال است این تابع این کار را ازطریق انتساب آدرس پنجره ی بعد به فیلد activeWindow انجام می دهد. اما چگونه باید بدانیم پنجره ی بعد کدام است و چگونه باید به آن دسترسی پیدا کنیم؟
در اینجا یک فیلد استاتیک دیگر از نوع آرایه ای از اشاره گر ها ایجاد می کنیم و نام آن را allWindows می گذاریم.
class Window{
static int activeIndex;
static Window** allWindows;
static Window* activeWindow;
static int windowsCount;
{
به نحوه ی تعریف این آرایه دقت کنید . قبلا گفتیم نام آرایه خود یک اشاره گر است که به خانه ی اول (array[0]) اشاره میکند حال اگر این خانه ی اول خودش اشاره گر باشد پس نام آرایه باdد اشاره گری به اشاره گر باشد به همین دلیل است که در تعریف این فیلد دوبار از ستاره استفاده شده. اما این که چرا آریه ای از اشاره گر دلیلش همان دلیل اشاره گر بودن فیلد activeWindow است چرا که قرار نیست یک کپی از تمام پنجره ها داشته باشیم بلکه می خواهیم به خود پنجر ه ها دسترسی داشته باشیم.
این آرایه از طریق تابع سازنده ی کلاس Window اشاره گر this پنجره ی در حال ایجاد را به انتهای خود اضاف ميكند علاوه براین یک واحد هم به فیلد استاتیک دیگری به نام windowsCount که تعداد کل پنجر های موجود را ذخیره می کند اضاف ميشود از این پس از طریق این آرایه به تمام پنجره ی ایجاد شده دسترسی داريم اين تابع برای این که بداند پنجره ی بعدی کدام است از اندکس آرایه استفاده می کند پس باید در محلی اندکس پنجره ی فعال جاری را ذخیره کنیم یک فیلد استاتیک از نوع int هم برای این موضوع ایجاد ميكنيم. حال به بدنه ی تابع SelectNext() توجه کنید
Window* Window::SelectNext()
{
if(activeIndex+1==windowsCount)activeIndex=-1;
return SetActive(allWindows[++activeIndex]);
}
ابتدا بررسی میکنیم که آیا پنجره ی جاری همان آخرین پنجره است ؟ در این صورت با -1 قرار دادن فیلد activeIndex از دسترسی به خانه ی خارج از محدوده ی آرایه allWindows جلو گیری میکنیم در دستور بعد به کمک تابعی دیگر به نام SetActive پنجره ی بعدی را فعال می کنیم. به این ترتیب
نکته:
توابع SelectNext() و SetActive() توابع عضو استاتیک عمومی هستند لذا فاقد اشاره گر this هستند پس دسترسی به فیلدها در این توابع در واقع به صورت
عضوWindow::
انجام شده به عبارتی دیگر توابع استاتیک به تمام اعضا استاتیک کلاس دسترسی دارند. البته به سایر اعضا هم دسترسی دارند ,اما فقط از طریق اشیا ایجاد شده. پس زمانی که هیچ شئی در توابع استاتیک قابل دستیابی نباشد دسترسی به فیلد ها و توابع عضو غیر استاتیک ممکن نیست.
احتمالا تا به حال متوجه شدید که تمام فیلد های استاتیک تعریف شده با دستیابی private تعریف شده اند.(در صورت عدم اعلان سطح دستیابی به اعضا پیش فرض private خواهد شد) علت این است که کاربر نباید بتواند به صورت مستقیم تغییری در این فیلد ها ایجاد . مثلا فرض کنید فیلد windowsCount مقدارد هی شود در این صورت عملکر تابع SelectNext() مختل خواهد شد. اما ممکن است کاربر نياز داشته باشد این فیلد را بخواند. پس میتوانیم تابع استاتیک عمومی دیگری به نامCount() تعریف کنیم که این فیلد را بر گرداند.
int Window::Count(){
return Window::windowsCount;
}
اگر یادتان باشد به این گونه توابع توابع دسترسی می گفتیم.
حال باید یک تابع هم برای خواندن activeWindow داشته باشیم این تابع را با نام ActiveWindow() و به صورت inline در خود بدنه ی کلاس تعریف میکنیم.
static Window* ActiveWindow(){return activeWindow;};
بدنه ی کلاسمان تا کنون به صورت
class Window{
static int activeIndex;
static Window** allWindows;
static Window* activeWindow;
static int windowsCount;
int left,top,width,height,borderColor,backColor,index;
void AddWindow();
void InitActiveWindow();
public:
static void PaintAll();
static int Count();
static Window* ActiveWindow(){return activeWindow;};
static Window* SetActive(Window* win);
static Window* SelectNext();
{
حال به بررسی اعضا مورد نیاز برای پیاده سازی موارد 4تا6 می پردازیم.
مشخصات کلی هر پنجره به صورت تعدادی فیلد private به صورت
int left,top,width,height,borderColor,backColor,index;
در تعریف کلاس ذکر شده. borderColor رنگ کادر و ,backColor رنگ پس زمنیه ی پنجره را مشخص میکند.index هم شماره ی پنجره است که می تواند مفید باشد.
توابع مفیدی برای دسترسی به این فیلد ها به صورت
int Left(){return this->left;}
int Top(){return this->top;}
void Init(int l,int t,int w,int h,int border,int back);
void SetColor(int border,int back);
void SetPosition(int left,int top);
void SetSize(int width,int height);
با سطح دسترسی public ایجاد شده اند واضح است که تغییر موقعیت رنگ و اندازه ی پنجره ها نیازمند ترسیم مجدد پنجره است لذا هر یک از این توابع که با عث این گونه تغییرات شوند تابع دیگری را به نام تابع Paint فر خوانی میکنند تا ظاهر پنجره دوباره ترسیم شود. اما مشکل این است که تغییر موقعیت یا اندازه کمی پیچده تر است برای جابجایی یک پنجره نیاز داریم که ابتدا پنجره پاک شود و سپس در موقعیت جدیدش ترسیم شود اما مشکل اینجاست که اگر پنجره ای در زیر پنجره ی مذکور باشد پاک می شود و خلاصه این که ظاهر سای پنجره ها خراب میشود. ساده ترین و بدترین راه حل این مشکل پاک کردن تمام صفحه نمایش و رسم مجدد تمام پنجره هاست. این عمل توسط تابع PaintAll() انجام میشود.
void Window::PaintAll(){
تنظیم رنگ سفید روی مشکیtextattr(15|0*16); //
پاک کردن صفحه نمایشclrscr();//
رسم تمام پنجره ها به غیر از پنجره ی فعالfor(int i=0;i
if(activeWindow!=allWindows[i])
allWindows[i]->Paint();
رسم پنجره ی فعالactiveWindow->Paint(); //
}
پنجره ی فعال آخر از همه رسم میشود چرا که باید برروی تمام پنجره ها قرار بگیرد. با توجه به این که لزومی ندارد هر پنجره یک نسخه از این تابع را داشته باشد این تابع را به صورت استاتیک تعریف میکنیم اما این که این تابع عمومی باشد یا خصوصی واقعا به نظر برنامه نویس بستگی دارد. عمومی بودن این تابع نمی تواند اختلالی در عملکرد کلاس ایجاد کند پس خوب است عمومی باشد!
تابع Paint هم برای ترسیم پنجره ها از سه تابع کمکی غیر عضو که هیچ ارتباطی با کلاس ندارند استفاده می کند.
void Window::Paint()
{
رسم پس زمینه پنجرهbfbox(this->left,this->top,this->width,this->height);//
آیا پنجره فعال استif(activeWindow==this)//
رسم کادر دو لبهbox2(this->left,this->top,this->width,this->height);//
else
رسم کادر ساده//box1(this->left,this->top,this->width,this->height);
}
در پایان به نحوه ی استفاده از این کلاس در برنامه توجه کنید.
int main(){
textattr(15||0*16);
clrscr();
Window *tmpWin;
for(int i=0;i<4;i++){ //ایجاد چهار پنجره ی جدید
tmpWin=new Window();
tmpWin->Init(i+1,i+1,10+i,5+i,15,i+1);
}
Window::SetActive(tmpWin); //انتخاب اخرین پنجره به عنوان پنجره ی فعال
char key=0;
while(key!=27) //Exit if Esc pressed.
{
key=getch();//خواندن یک کلید از صفحه کلید
if(key==9) //Select next window if tab pressed.
{
Window::SelectNext();
}
else if(key>='0' && key<='7')//تغییر رنگ پنجره فعال با فشردن کلید های 0تا7
{
Window::ActiveWindow()->SetColor(15, key-'0');
}
else if((key=='n' || key=='N')&&(Window::Count()<20)) //Create new window
{
i=Window::Count()+1;
Window *newWin= new Window(i,i,i+10,i+5);
newWin->SetColor(15,i%8);
Window::SetActive(newWin); //or newWin->SetActive(newWin);
}
key=getch();//خواندن یک کلید از صفحه کلید
if(key==NULL){ //در این صورت کلید تو سعه یا فته شامل کلید های جهتی است
key=getch();//خواندن کاراکتر دوم کلید توسعه یا فته
Window *win=Window::ActiveWindow();//ذخیره ی یک اشاره گر به پنجره ی فعال
//به منظور تایپ کمتر
if(key=='P') //شناسایی کلید های جهتی و انتقا پنجره فعال به موقعیت جدید
win->SetPosition(win->Left(),win->Top()+1);
else if(key==’M’)
win->SetPosition(win->Left()+1,win->Top());
else if(key==’H’)
win->SetPosition(win->Left(),win->Top()-1);
else if(key==’K’)
win->SetPosition(win->Left()-1,win->Top());
}
}
return 0;
}
نکته:
تابع getch() کد کلید فشرده شده را برمی گرداند تعدادی از کلید ها دارای کدهای خاص به نام کد توسعه یافته هستند. این کدها بر خلاف کد های معمولی که یک بایتی (یک کاراکتری) هستند دو بایتی هستند بایت اول این کدها عدد صفر یا همان ثابت NULL است و بایت دوم کد کلید. کلید های جهتی از جمله این کلید ها هستند. تابع getch()در هر فراخوانی یک بایت را می خواند در این حالت بایت دوم در محلی به نام بافر صفحه کلید(یک حافظه ی میانی) قرار می گیرد در این حالت با دومین فراخوانی تابع بایت دوم در لحظه خواند می شود یعنی تابع getch() منتظر فشردن کلیدی نمی ماند و بایت با قیمانده را می خواند. پس برای تشخیص کلید های جهتی باید بررسی کنیم اگر getch() مقدار NULL را برگرداند بایت دوم را با فراخوانی مجدد بخوانیم و کلید را شناسایی کنیم.
خب حالا وقت این رسیده که بگم چرا یک کلاس بصری رو به عنوان مثال بررسی کردیم. علت اینه که می تونیم فرق بین کپی یک شی و اشاره گر به شی را به چشم ببینیم
Window win(2,2,5,7);
Window *ptrWin=&win;
ptrWin->SetPosition(4,4);
Window win2=win;
win2.SetPosition(1,1);
دستور اول یک پنجره ایجاد می کند. دودستور بعد یک اشاره گر به شی win ایجاد می کند. سپس موقیت شی را از طریق این اشاره گر تغییر می دهیم می بینم که خود شی win برروی مانیتور جابجا می شود. اما در دستور بعد یک کپی از ٌwin به عنوان win2 ایجاد می شود با تغییر مکان win2 به وضوح در مانیتور مشاهده می شود که دو پنجره وجود دارد. یک مثال بهتر زمانی است که یک تابع ,دستور return ی به شکل زیر داشته باشد که یک کپی از شی مورد اشاره ptrWin ایجاد می کند و به برنامه فراخوان بر میگرداند. یا مثال دیگر ارسال اشیا به توابع از طریق آرگمان هاست آن هم بدون استفاده از اشاره گر ها و یا رفرنس ها.
به همین علت است که آرگمان ها و مقادیز بازگشتی توابع را در صورت شی بودن به صورت اشاره گر یا رفرنس در نظر می گیرند.
return *ptrWin;
تذکر:
به منظور جلو گیری از طولانی تر شدن این مطلب پیاده سازی کلاس String جلسه ی آینده بررسی میکنیم.



