[DForm]我也來做自定義Winform之另類標題欄重繪

来源:http://www.cnblogs.com/encoding/archive/2016/06/21/5603080.html
-Advertisement-
Play Games

據說得有楔子 按照慣例,先來幾張樣例圖(註:為了展示視窗陰影效果,截圖範圍向外擴展了些,各位憑想象吧)。 還要來個序 其實,很多年沒寫過Winform了,前端時間在重構我們公司自己的呼叫中心系統,突然就覺得客戶端好醜好醜,對於我這種強迫症晚期患者來說,界面不好看都不知道怎麼寫代碼的,簡直就是種折磨, ...


據說得有楔子

    按照慣例,先來幾張樣例圖(註:為了展示視窗陰影效果,截圖範圍向外擴展了些,各位憑想象吧)。

    image       image    image    image   image

 

 

 

 

還要來個序

   其實,很多年沒寫過Winform了,前端時間在重構我們公司自己的呼叫中心系統,突然就覺得客戶端好醜好醜,對於我這種強迫症晚期患者來說,界面不好看都不知道怎麼寫代碼的,簡直就是種折磨,還是滿清十大酷刑級別那種。

   很多人推薦WPF,不過個人對WPF沒啥感覺,而且據說也無法支持2.0,還是採用Winform技術來搞吧。

終於第一節

   做Winform皮膚,我瞭解的無非有2種方式。

   1.採用純圖片模式:由專業美工設計各種樣式的圖片,進行視窗背景圖片設置

   2.採用GDI+純代碼繪製:參照美工設計的各種樣式的圖片,使用C#代碼繪製出來

   第一種方式很簡單,但是可復用性不高,性能上面應該也會有一些影響,如果圖片太大,視窗拖動等引起的重繪時,會明顯有閃爍等情況。

   第二種方式很複雜,但是效率和復用性高,開放各種擴張屬性之後,可以適用於大部分場景。

   以前用了很多次第一種方案,每次新做APP,都得重新設計界面,很不便利。這次,我準備採用第二種方案來做一個公用的皮膚。

 

   關於GDI+,我只能算一個新人,不做具體的介紹,這裡只講我的一些實現方式,計劃項目完成後,開源到github。

繪製標題欄

   做自定義界面,繞不開一個問題就是繪製標題欄。

   每個Winform窗體,可以分為兩個部分:非客戶區域和客戶區域。

   非客戶區域:表示無法由我們程式猿繪製的部分,具體包括:視窗標題欄,邊框

   客戶區域:表示由我們程式猿繪製的部分,也就是窗體內容,平時我們拖控制項都是拖到客戶區域

   一般自定義視窗的實現方式無非以下種

   1.設置視窗為無邊框視窗,頂部放一個Panel,設置Panel.Dock=Top,然後在Panel裡面繪製logo、標題、按鈕等元素。

   2.攔截視窗消息,重寫WndProc方法,攔截視窗標題繪製消息,由自己手工繪製

  很多人會為了簡便,採用第一種方式,不過缺點比較明顯,對於我來說,最主要的一點就是真正的實現界面,裡面的控制項元素Dock會受到影響,不利於客戶區域佈局。

  高手牛人會採用第二種方式,不是我這種Winform小白的菜,所以,我採用第三種方式,也是本篇文章的核心思想。

    採用無邊框視窗,設置視窗Padding.Top為標題欄高度,採用GDI+繪製標題欄元素。

   這種方式的好處顯而易見

      具體實現窗體子控制項Dock不受影響

      無邊框之後,重寫窗體拖動事件不需要對標題欄每一個元素進行事件處理

      標題欄高度可隨時自定義

   本文開頭的幾個截圖,標題欄繪製代碼如下

 繪製標題文字、Logo圖片

        private void DrawTitle(Graphics g)
        {
            var x = 6 + this.GetBorderWidth();
            if (this.ShowLogo)
            {
                g.SmoothingMode = SmoothingMode.AntiAlias;
                ImageAttributes imgAtt = new ImageAttributes();
                imgAtt.SetWrapMode(System.Drawing.Drawing2D.WrapMode.TileFlipXY);
                using (var image = this.Icon.ToBitmap())
                {
                    var rec = new Rectangle(x, (this.captionHeight - 24) / 2, 24, 24);

                    g.DrawImage(image, rec, 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, imgAtt);
                }

            }

            if (this.ShowTitle)
            {
                var font = this.titleFont == null ? this.Font : this.titleFont;
                var fontSize = Size.Ceiling(g.MeasureString(this.Text, font));
                if (this.CenterTitle)
                {
                    x = (this.Width - fontSize.Width) / 2;
                }
                else if (this.ShowLogo)
                {
                    x += 30;
                }

                using (var brush = new SolidBrush(this.CaptionForeColor))
                {
                    g.DrawString(this.Text, font, brush, x, (this.CaptionHeight - fontSize.Height) / 2 + this.GetBorderWidth());
                }
            }
        }

  

 繪製最小化、最大化、關閉、幫助按鈕

        private void DrawControlBox(Graphics g)
        {
            if (this.ControlBox)
            {
                ImageAttributes ImgAtt = new ImageAttributes();
                ImgAtt.SetWrapMode(System.Drawing.Drawing2D.WrapMode.TileFlipXY);
                var x = this.Width - 32;
                //var rec = new Rectangle(this.Width - 32, (this.CaptionHeight - 32) / 2 + this.BorderWidth, 32, 32);
                //var rec = new Rectangle(x, this.BorderWidth, 32, 32);
                if (this.CloseButtonImage != null)
                {
                    closeRect = new Rectangle(x, 0, 32, 32);
                    using (var brush = new SolidBrush(closeHover ? this.ControlActivedColor : this.ControlBackColor))
                    {
                        g.FillRectangle(brush, closeRect);
                    }

                    g.DrawImage(this.CloseButtonImage, closeRect, 0, 0, this.CloseButtonImage.Width, this.CloseButtonImage.Height, GraphicsUnit.Pixel, ImgAtt);
                    x -= 32;
                }

                if (this.MaximizeBox && this.WindowState == FormWindowState.Maximized && this.MaximumNormalButtonImage != null)
                {
                    maxRect = new Rectangle(x, 0, 32, 32);

                    using (var brush = new SolidBrush(maxHover ? this.ControlActivedColor : this.ControlBackColor))
                    {
                        g.FillRectangle(brush, maxRect);
                    }

                    g.DrawImage(this.MaximumNormalButtonImage, maxRect, 0, 0, this.MaximumNormalButtonImage.Width, this.MaximumNormalButtonImage.Height, GraphicsUnit.Pixel, ImgAtt);
                    x -= 32;
                }
                else if (this.MaximizeBox && this.WindowState != FormWindowState.Maximized && this.MaximumButtonImage != null)
                {
                    maxRect = new Rectangle(x, 0, 32, 32);
                    using (var brush = new SolidBrush(maxHover ? this.ControlActivedColor : this.ControlBackColor))
                    {
                        g.FillRectangle(brush, maxRect);
                    }
                    g.DrawImage(this.MaximumButtonImage, maxRect, 0, 0, this.MaximumButtonImage.Width, this.MaximumButtonImage.Height, GraphicsUnit.Pixel, ImgAtt);
                    x -= 32;
                }

                if (this.MinimizeBox && this.MinimumButtonImage != null)
                {
                    minRect = new Rectangle(x, 0, 32, 32);

                    using (var brush = new SolidBrush(minHover ? this.ControlActivedColor : this.ControlBackColor))
                    {
                        g.FillRectangle(brush, minRect);
                    }
                    g.DrawImage(this.MinimumButtonImage, minRect, 0, 0, this.MinimumButtonImage.Width, this.MinimumButtonImage.Height, GraphicsUnit.Pixel, ImgAtt);
                    x -= 32;
                }

                if (base.HelpButton && this.HelpButtonImage != null)
                {
                    helpRect = new Rectangle(x, 0, 32, 32);
                    using (var brush = new SolidBrush(helpHover ? this.ControlActivedColor : this.ControlBackColor))
                    {
                        g.FillRectangle(brush, helpRect);
                    }
                    g.DrawImage(this.HelpButtonImage, helpRect, 0, 0, this.HelpButtonImage.Width, this.HelpButtonImage.Height, GraphicsUnit.Pixel, ImgAtt);
                    x -= 32;
                }
            }
        }

  

窗體OnPaint事件,自繪標題欄

protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            #region draw caption
            using (var brush = new SolidBrush(this.CaptionBackgroundColor))
            {
                e.Graphics.FillRectangle(brush, captionRect);
            }

            this.DrawTitle(e.Graphics);
            this.DrawControlBox(e.Graphics);
            #endregion

            #region draw border
            ControlPaint.DrawBorder(e.Graphics, this.ClientRectangle, borderColor, ButtonBorderStyle.Solid);
            #endregion
        }

  採用Padding來約束子實現界面的元素佈局位置

   當我採用了無邊框窗體來做自定義皮膚之後,由於去除了非客戶區域(標題欄、邊框),子實現窗體的坐標位置(0,0)實際上應該會覆蓋我的標題欄,不過,反編譯.NET源碼之後,我發現Form類有一個Padding屬性,這個屬性繼承自Control類,它的作用與CSS中的padding相同。所以,我決定使用這個技術來約束子實現界面的元素佈局位置。

   每次修改標題欄高度時,需要重新生成窗體的Padding屬性

        private int captionHeight;
        [Category("標題欄"), Description("標題欄高度"), DefaultValue(typeof(int), "40")]
        public int CaptionHeight { get { return this.captionHeight; } set { this.captionHeight = value; this.SetPadding(); } }

 

 每次修改邊框寬度時,需要重新生成窗體的Padding屬性

        private int borderWidth;
        [Category("邊框"), Description("邊框寬度"), DefaultValue(typeof(int), "1")]
        public int BorderWidth { get { return this.borderWidth; } set { this.borderWidth = value; this.SetPadding(); } }

  最後,隱藏掉Padding屬性,外部修改無效

public new Padding Padding { get; set; }

  附加1:標題欄自繪按鈕懸浮背景色修改和單擊事件處理

        protected override void OnMouseMove(MouseEventArgs e)
        {
            Point p = new Point(e.X, e.Y);
            captionHover = captionRect.Contains(p);
            if (captionHover)
            {
                closeHover = closeRect != Rectangle.Empty && closeRect.Contains(p);
                minHover = minRect != Rectangle.Empty && minRect.Contains(p);
                maxHover = maxRect != Rectangle.Empty && maxRect.Contains(p);
                helpHover = helpRect != Rectangle.Empty && helpRect.Contains(p);
                this.Invalidate(captionRect);
                this.Cursor = (closeHover || minHover || maxHover || helpHover) ? Cursors.Hand : Cursors.Default;
            }
            else
            {
                if (closeHover || minHover || maxHover || helpHover)
                {
                    this.Invalidate(captionRect);
                    closeHover = minHover = maxHover = helpHover = false;
                }

                this.Cursor = Cursors.Default;
            }

            base.OnMouseMove(e);
        }

protected override void OnMouseClick(MouseEventArgs e)
{
var point = new Point(e.X, e.Y);
if (this.closeRect != Rectangle.Empty && this.closeRect.Contains(point))
{
this.Close();
return;
}

if (!this.maxRect.IsEmpty && this.maxRect.Contains(point))
{
if (this.WindowState == FormWindowState.Maximized)
{
this.WindowState = FormWindowState.Normal;
}
else
{
this.WindowState = FormWindowState.Maximized;
}

this.maxHover = false;
return;
}

if (!this.minRect.IsEmpty && this.minRect.Contains(point))
{
this.WindowState = FormWindowState.Minimized;
this.minHover = false;
return;
}

if (!this.helpRect.IsEmpty && this.helpRect.Contains(point))
{
this.helpHover = false;
this.Invalidate(this.captionRect);
CancelEventArgs ce = new CancelEventArgs();
base.OnHelpButtonClicked(ce);
return;
}

base.OnMouseClick(e);
}


  

附加2:處理無邊框窗體用戶調整大小

#region 調整視窗大小
        const int Guying_HTLEFT = 10;
        const int Guying_HTRIGHT = 11;
        const int Guying_HTTOP = 12;
        const int Guying_HTTOPLEFT = 13;
        const int Guying_HTTOPRIGHT = 14;
        const int Guying_HTBOTTOM = 15;
        const int Guying_HTBOTTOMLEFT = 0x10;
        const int Guying_HTBOTTOMRIGHT = 17;

        protected override void WndProc(ref Message m)
        {
            if (this.closeHover || this.minHover || this.maxHover || this.helpHover)
            {
                base.WndProc(ref m);
                return;
            }

            if (!this.CustomResizeable)
            {
                base.WndProc(ref m);
                return;
            }
            switch (m.Msg)
            {
                case 0x0084:
                    base.WndProc(ref m);
                    Point vPoint = new Point((int)m.LParam & 0xFFFF,
                        (int)m.LParam >> 16 & 0xFFFF);
                    vPoint = PointToClient(vPoint);
                    if (vPoint.X <= 5)
                        if (vPoint.Y <= 5)
                            m.Result = (IntPtr)Guying_HTTOPLEFT;
                        else if (vPoint.Y >= ClientSize.Height - 5)
                            m.Result = (IntPtr)Guying_HTBOTTOMLEFT;
                        else m.Result = (IntPtr)Guying_HTLEFT;
                    else if (vPoint.X >= ClientSize.Width - 5)
                        if (vPoint.Y <= 5)
                            m.Result = (IntPtr)Guying_HTTOPRIGHT;
                        else if (vPoint.Y >= ClientSize.Height - 5)
                            m.Result = (IntPtr)Guying_HTBOTTOMRIGHT;
                        else m.Result = (IntPtr)Guying_HTRIGHT;
                    else if (vPoint.Y <= 5)
                        m.Result = (IntPtr)Guying_HTTOP;
                    else if (vPoint.Y >= ClientSize.Height - 5)
                        m.Result = (IntPtr)Guying_HTBOTTOM;
                    break;
                case 0x0201:                //滑鼠左鍵按下的消息   
                    m.Msg = 0x00A1;         //更改消息為非客戶區按下滑鼠   
                    m.LParam = IntPtr.Zero; //預設值   
                    m.WParam = new IntPtr(2);//滑鼠放在標題欄內   
                    base.WndProc(ref m);
                    break;
                default:
                    base.WndProc(ref m);
                    break;
            }
        }
        #endregion

  全類文件,不曉得咋上傳附件,所以沒傳,要的可以找我QQ。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Winform中Treeview控制項失去焦點,將選擇的節點設置為高亮顯示 (2012-07-16 13:47:07)轉載▼標簽: winform treeview drawnode Treeview控制項--Name:tVtypeList將tVtypeList的HideSelection屬性設置為Fa ...
  • 前言 繼之前發的帖子【ORM-Dapper+DapperExtensions】,對Dapper的擴展代碼也進行了改進,同時加入Dapper 對Lambda表達式的支持。 由於之前缺乏對Lambda的知識,還是使用了拿來主義。研究了些案例,總歸有些問題: 1、只能生成sql、不能將值進行參數化。 2、 ...
  • 天創恆達UB530高清視頻採集卡USB游戲PS4視頻翻錄電影網路直播錄播會議HDMI採集盒 http://item.jd.com/1567495458.html 天創恆達UB5A0 USB採集卡HDMI 分量 網路會議ps4游戲視頻高清採集盒1080p http://item.jd.com/1542 ...
  • 我們在前面章節中講了寄宿,在前面的實例中也用到了配置文件,這一篇主要講講如何在應用配置文件,提高WCF程式的靈活性。在編寫WCF服務應用程式時,編寫配置項也是其中一項主要工作,在前面的幾個示例中我也使用過配置文件,通過配置文件來簡化代碼。WCF通過公開終結點,向客戶端公開服務,包括服務的地址、服務用... ...
  • 原文: "Adding Validation" 作者: "Rick Anderson" 翻譯: "謝煬(Kiler)" 校對: "孟帥洋(書緣)" 、 "婁宇(Lyrics)" 、 "許登洋(Seay)" 在本章節中你將為 模型類添加驗證邏輯,以確保在用戶試圖創建或編輯影片數據時強制執行驗證規則。 ...
  • “廠長,上一次我們講過了工作流的整體規劃,今天我要動手做啦!我想先把工作流的自定義表單做出來。” “好的,以前我做這方面的東西,我給你設計了一份表結構,你先拿去看看。” “廠長,是不是沒發完,怎麼就一個表?” “我就知道你會這麼問,我現在給你解釋一下重點欄位的含義。” 數據表:將表單上的內容保存到哪 ...
  • 1. 引言 在實際的項目中,樹還是用的比較多的一種,尤其是對於具有層次結構的數據。相信很多人都學過樹的遍歷,比如先序遍歷,後序遍歷等,利用遞歸還是很容易理解的。 今天給大家介紹下二叉樹的幾種遍歷演算法,包括遞歸和非遞歸的實現。 首先建立一棵二叉樹 如: 一棵簡單的二叉樹 2. 先序遍歷 先序遍歷還是很 ...
  • 1、自動屬性,就是一個屬性的簡寫方式,不過後臺編譯時會自動的將屬性的代碼補全 2、可變類型 Var: 其實不是真的可變,而是對於可以推斷出來的強類型對象可以幫我們自動識別。在聲明太複雜的類型是可以使用。或者使用次數不多的情況下 Var編譯時確定類型 Dynamic : 真正的可變類型,在運行階段看你 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...