استفاده از منوهای آبشاری کافیه

مقدمه

این مقاله درباره منوها و نوار ابزارها در برنامه های ویندوزی است و مطالب آن درباره روش های طراحی منو در برنامه های ویندوزی است که خیلی کمتر به آن پرداخته شده است. بسیاری از برنامه های کاربردی ویندوزی کنترل های سفارشی خود را دارند که از لحاظ شکل و شمایل یا تم ظاهری (Theme) با سایر اجزای برنامه کاربردی هماهنگ می باشد و سفارشی سازی شده است. این مقاله روش هایی را برای سفارشی سازی منو ها معرفی می کند که با کمک آن می توانید تم کلاسیک برنامه را تغییر دهید و تجربه کاری متفاوتی را به کاربر بدهید. ابتدا به نحوه چیدمان آیتم های منو می پردازد و پس از آن روشی برای سفارشی کردن Theme منوها یا رندر Rendering کردن آنها در برنامه های ویندوزی معرفی می کند.

در ادامه منویی که در تصویر زیر مشاهده می کنید را در یک برنامه ویندوزی (Winforms) پیاده سازی می کنیم. این کار به کمک خاصیت LayoutStyle امکان پذیر است. هدف معرفی کلاس TableLayoutSettings و استفاده از نوع ToolStripLayoutStyle.Table برای LayoutStyle است.

منوی سفارشی که در این مقاله طراحی می کنیم.
منوی سفارشی که در این مقاله طراحی می کنیم.

بعد از آن به سراغ ToolStripControlHost می رویم و همان منو را به روشی دیگر پیاده سازی می کنیم. هدف از معرفی این کلاس آشنایی با روش های دیگر برای پیاده سازی منو هایی با طراحی متفاوت و پیچیده تر است و اینکه چطور می توانیم یک کامپوننت سفارشی (UserControl) را به صورت منو یا اصطلاحا Popup نمایش دهیم. در انتها نیز نحوه سفارشی سازی یک رندرینگ حرفه ای (Rendering) برای پیاده سازی تم ظاهری (Theme) مخصوص را معرفی می کنم.

کاربرد ToolStripControlHost

کاربرد این که بخواهیم یک کنترل سفارشی (User Control) به صورت Popup نمایش دهیم کجاست؟ فرض کنید که شما یک کنترل ساعت درست کرده اید وقتی که کاربر روی آن کلیک می کند یک فرم انتخاب ساعت برایش نمایش داده می شود که بعد از انتخاب ساعت مقدار آن در TextBox مربوط به آن نمایش داده می شود دقیقا مانند شکل زیر. این کار به کمک ToolStripControlHost به سادگی امکان پذیر است.
کامپوننت سفارشی برای انتخاب ساعت
کامپوننت سفارشی برای انتخاب ساعت


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

کلاس هایی که از ToolStrip ارث بری کرده اند.
کلاس هایی که از ToolStrip ارث بری کرده اند.

کلاس ToolStripDropDown کنترل مناسبی برای ایجاد یک منوی سفارشی و کاربر پسند است. با ارث بری از این کلاس می توان قابلیت های جالبی را به آن اضافه کرد. در این کلاس خصوصیتی به نام LayoutStyle وجود دارد که به کمک آن میتوان نحوه چیدمان و آرایش آیتم های داخل منو (کنترل های منو) را تغییر داد. به کمک ارث بری و سفارشی سازی می توان رفتاری های پیش فرض یک کنترل را تغییر داد و قابلیت های آن را ارتقا داد؛ کاری که بیشتر شرکت هایی که در زمنیه کامپوننت های برنامه نویسی فعالیت دارند؛ انجام می دهند مثل DevExpress ، Telerik ، Krypton و غیره.

شروع کدنویسی

در ابتدا ما یک منوی سفارشی را با ارث بری از ToolStripDropDown درست می کنیم. هدف از این کار این است که منوی بازشونده ای درست کنیم که بتوانیم تعدادی آیتم (ToolStripItem) را به صورت دلخواه در یک ساختار جدول مانند روی آن قرار دهیم.

public class GridMenu : ToolStripDropDown {
           public GridMenu(){
                     InitializeComponent();
           }
}

حالا به کمک خاصیت LayoutStyle که مقدار آن را به ToolStripLayoutStyle.Table تنظیم می کنیم. سطح منو را به صورت یک جدول یا گرید که مجموعه ای از سطرها و ستون ها است در می آوریم و آیتم های منو را در هر خانه قرار می دهیم. در این حالت سطح منو به صورت شکل زیر در میاد.

سطح منو به صورت جدول یا  گرید
سطح منو به صورت جدول یا گرید

خاصیت LayoutStyle به سه صورت Flow, Stack و Table می باشد. در اینجا ما از Table استفاده می کنیم تا حالت منوی آبشاری را تغییر دهیم. برای مطالعه بیشتر درباره دوتا حالت دیگر می توانید به مستندات مایکروسافت در رابطه با چیدمان عناصر مراجعه کنید.

public class GridMenu : ToolStripDropDown
{
        private Size                                  _tileSize                 = new Size(50, 50);
        private TableLayoutSettings     _tableSettings      = null; 
        public GridMenu ()
        {
            InitializeComponent();
        }
        private void InitializeComponent()
        {
            this.SuspendLayout();
            LayoutStyle         	      = ToolStripLayoutStyle.Table;
            _tableSettings                 = base.LayoutSettings as TableLayoutSettings; 
            this.ResumeLayout(false);
        }
}

وقتی که خاصیت LayoutStyle را به صورت ToolStripLayoutStyle.Table تنظیم می کنیم در این حالت می توانیم LayoutSettings را به کلاس TableLayoutSettings تبدیل کنیم (Casting) و برای هر آیتم محل چیدمان را مشخص کنیم. این کار به کمک کلاس TableLayoutPanelCellPosition امکان پذیر است.

به متد زیر دقت کنید.

public void AddItem()
{ 
      this.Size = new Size(            
        (_columns * _tileSize.Width)  + (_margin.Left + _margin.Right)  * _columns + _margin.Right,            
        (_rows * _tileSize.Height) + (_margin.Top + _margin.Bottom) * _rows + _margin.Bottom         
       );  
    var btnGoogle = new ToolStripButton (&quotGoogle&quot, Properties.Resources.google, null, &quotbtnGoogle&quot); 
     Items.Add(btnGoogle);  
     var btnGoogleCellPos = new TableLayoutPanelCellPosition(0, 0);
     _tableSettings.SetCellPosition(btnGoogle, btnGoogleCellPos);
}

در این متد یک آیتم از نوع ToolStripButton را به منو اضافه کرده ایم و به کمک کلاس TableLayoutPanelCellPosition مشخص می کنیم که در کدام خانه جدول از منو قرار بگیرد به کد دقت کنید. در کد بالا ابتدا سایز کل سطح منو را مشخص می کنیم ( با توجه به تعداد سطرها و ستون ها و اینکه هر خانه از جدول چه ابعادی دارد و اینکه فاصله یا Margin هر خانه از همدیگر چقدر باشد). سپس یک آیتم از نوع ToolStripButton تعریف کرده ایم و به لیست آیتم های موجود اضافه کرده ایم. در نهایت به کمک کلاس TableLayoutPanelCellPosition مشخص کرد ایم که در اولین خانه از جدول قرار بگیرد. در صورتی که بخواهیم که یک آیتم در منو به صورت افقی یا عمودی کشیده شود یا اینکه چند خانه را با هم دربر بگیرد از متدهای SetColumnSpan و SetRowSpan استفاده می کنیم.

تا اینجا ما فقط منوی مورد نظر را ایجاد کرده ایم ولی برای اینکه بتوانیم در یک فرم از آن استفاده کنیم کافی است که بعد از قرار دادن منوی روی فرم آنرا به یک MenuStrip یا ToolStrip اختصاص بدهیم. به کد زیر دقت کنید که در سازنده فرم نوشته شده است :

public partial class FormMain : Form    {
        public FormMain()
        {
            InitializeComponent();
            gridMenu2.AddItem(); 
            toolStripDropDownButton1.DropDown = gridMenu2; 
        }
  }

در کد بالا بعد از فراخوانی متد AddItem که آیتم های منو را اضافه می کند و هر کدام را در جای خودش قرار میدهد(اصطلاحا Initializing) ؛ منویی را که درست کرده ایم را به یک toolStripDropDownButton1 اختصاص داده ایم تا در صورتی که روی آن کلیک شد منوی مورد نظر ما را نمایش دهد. نتیجه را در تصویر زیر می توانید ببینید.

نتیجه طراحی منو به صورت گرید
نتیجه طراحی منو به صورت گرید

لینک کامل سورس برنامه برای نمایش منو در حالتهای مختلف را در گیت هاب من می توانید مشاهده کنید در صورتی که سوال داشتید در قسمت Issues مطرح کنید.

GitHub: https://github.com/SoranRad/GridMenu

استفاده از ToolStripControlHost

این قسمت به کلاس ToolStripControlHost می پردازد و به کمک آن همان منوی قبلی را طراحی می کند. استفاده از کلاس ToolStripControlHost روش دوم برای ساخت منوی دلخواه با چیدمانی متفاوت است . مثل روش قبل یک کلاس را از ToolStripDropDown ارث بری می کنیم :

    public class PopupControl : ToolStripDropDown
    {
        public PopupControl()
        {
            InitializeComponent();
        }
    }

حالا باید یک UserControl درست کنیم و کنترل هایی رو که می خواهیم روی آن قرار دهیم. به شکل زیر دقت کنید. چند کنترل دکمه را روی یک UserControl قرار داده ایم.

کنترل سفارشی (UserControl) برای نمایش در یک منو
کنترل سفارشی (UserControl) برای نمایش در یک منو

حالا کافی است که متد AddItem را همانند روش قبل به صورت زیر تغییر بدهیم.

public void AddItem()
        {
            var control = new CustomControl()
            {
                Name = &quotpopupControl1&quot,
                Size = new Size(304, 258)
            };   
            Host = new ToolStripControlHost(control)
            {
                Margin              = Padding.Empty,
                Padding             = Padding.Empty,
                AutoSize            = false,
                AutoToolTip         = false,
                DoubleClickEnabled  = true,
            };
            this.Size = new Size(
                control.Width+10,
                control.Width+10
            );
            Host.Size = new Size(control.Size.Width, control.Size.Height);
            control.Anchor = AnchorStyles.Bottom
                             | AnchorStyles.Left
                             | AnchorStyles.Right
                             | AnchorStyles.Top;
            this.Items.Clear();
            this.Items.Add(Host);
        }

در کد بالا ابتدا یک نمونه از کنترل سفارشی UserControl را نمونه سازی کرده ایم و بعد از آن به کمک ToolStripControlHost این کنترل را به مجموعه آیتم های منو اضافه کرده ایم در اصل از منو خواسته ایم که کنترل سفارشی مورد نظر را به صورت Popup نمایش دهد. کلاس ToolStripControlHost یک Host یا Container برای کنترل سفارشی ما محسوب می شود و وظیفه آن گرفتن یک کنترل و قرار دادن آن به عنوان یک آیتم منو در لیست آیتم هاست. نتیجه کد بالا را می توانید در تصویر زیر ببینید.

نمایش یک کنترل سفارشی در منو
نمایش یک کنترل سفارشی در منو

سورس کامل مربوط به این بخش را هم می توانید در آدرس گیت هابی که قبلا ذکر شد پیدا کنید. در این ریپازیتوری من مثال های خوبی از نحوه ساخت و استفاده از منوهای سفارشی رو ارائه کرده ام و می توانید از مثال های آن استفاده کنید.

نتیجه گیری

بسیاری از برنامه ها دیگر از منوهای آبشاری و کلاسیک استفاده نمی کنند و برای آنکه تجربه متفاوتی به کاربر حین استفاده از برنامه بدهند بسیاری از کنترل های پیش فرض فریمورک دات نت را مجددا توسعه و سفارشی سازی می کنند. با نگاهی گذرا به منوها و ToolBars و ToolStrip های برنامه هایی مانند FireFox , GoogleChrome , Office 365 و بسیاری دیگر از برنامه ها مشاهده می کنید که منوهای آن ها آیتم های بسیار متفاوتی را نمایش می دهند که کاربر در استفاده از آنها بسیار راحت تر است. در این مقاله سعی کرده ام به دو روش ساده، طراحی این سبک از منو ها را آموزش دهم.

قدم بعدی

در انتها قصد دارم که به دو نکته اساسی دیگر درباره آیتم های منو اشاره کنم که می تواند ظاهر برنامه ها را بسیار تحت تاثیر قرار دهد .

  • همانطور که در تصویر بالا مشاهده کردید کلاس ToolStripItem والد تمام آیتم هایی است که در منو نمایش داده می شود حالا چه آنها در MenuStrip یا ToolStrip یا حتی ToolStripDropDown قرار گرفته باشند. پس شما راحت می توانید با ارث بری از ToolStripItem یا حتی هر کدام از کلاس زیر مجموعه آن یک منوی کاملا سفارشی مختص به برنامه خود را داشته باشید و در برنامه های تان از آنها استفاده کنید.همانطور که من این کار در گیت هاب این مقاله انجام داده ام به آدرس زیر بروید.

https://github.com/SoranRad/GridMenu/blob/main/src/DotNetFramework/Components/TdToolStripButton.cs

  • نحوه ی رندر کردن منوهای برنامه خود را می توانید بسیار بیشتر سفارشی سازی کنید و شکل و شمایل منوها را در حالت های انتخابی یا هاور شده(Hover) یا غیرفعال شده(Disable) را سفارشی کنید. به شکل های زیر دقت کنید.
انواع تم  ظاهری با زندینگ حرفه ای
انواع تم ظاهری با زندینگ حرفه ای

هر کدام از تصاویر بالا روش رندر Rendering مختص به خود را دارد؛ برای پیاده سازی یک منو با ظاهر فوق می توانید یک کلاس سفارشی شده از ToolStripProfessionalRenderer پیاده سازی کنید و در برنامه تان از آنها استفاده کنید. بحث درباره این کلاس در حوصله این مقاله نمی باشد و خود نیاز به مطالب بیشتری دارد ولی برای مطالعه بیشتر می توانید به لینک های زیر مراجعه کنید.

مستندات مایکروسافت برای رندرینگ حرفه ای

برای مثال یک نمونه رندر سفارشی شده را در آدرس زیر می توانید مشاهده کنید.

https://github.com/SoranRad/Desktop-Application-Component-libraries/tree/main/src/MS_Control/Render