【C#】分享一個彈出容器層,像右鍵菜單那樣召即來揮則去


適用於:.net2.0+ Winform項目

------------------201508261813更新(源碼有更新、Demo未更新)------------------

  • 重新繪制調整大小手柄(SizeGrip,右下角那個),因為系統自繪的太靠邊角,在XP下會蓋過那部分邊框,視覺體驗不好。改進如圖:

    新增的DrawSizeGrip方法就是繪制方法,是protected virtual的,所以如果你看不上我畫的這個,可以在子類重寫該方法畫你自己滿意的(題外,畫這個我還參考了VS2010的效果,不過是相反的,VS的是凸起效果,我這是塌陷style)
  • 支持四邊+四角全方位拖動改變浮動層尺寸,改善體驗。如圖:

注:浮動層是否可以調整大小是根據SizeGripStyle屬性決定,分3種情況:

  • SizeGripStyle為Show,則始終允許用戶調整大小,手柄會出現、鼠標移至邊緣邊角會產生視覺變化並可以拖動
  • SizeGripStyle為Hide,則始終禁止用戶調整大小,手柄會出現、鼠標移至邊緣邊角會產生視覺變化,也不可以拖動改變大小
  • SizeGripStyle為Auto,則在模式化打開(Modal為true,即通過ShowDialog打開的)時與Show一致,非模式化打開(Modal為false,通過Show打開)時與Hide一致,這也是原版Form的邏輯,只不過原版Form還會根據FormBorderStyle,但本類已將該屬性固化,所以請注意Auto這貨,建議始終顯式指定Show/Hide為妙

------------------201508251458更新------------------

  • 激活首控件之前是在OnShown中進行,經過研究,改為令TopMost=true,就能使浮動層與正常窗體有一致的激活首控件行為,同時省卻了對OnShown的重寫
  • 解決子控件有時沒有聚焦框(焦點虛線框)的問題。如圖:

注:最后的demo沒更新,請重新取FloatLayerBase.cs源碼就好

------------------201508240846原文(已更新)------------------

背景:

有時候我們需要開一個簡單的窗口來做一些事,例如輸入一些東西、點選一個item之類的,可能像這樣:

完了返回原窗體並獲取剛剛的輸入,這樣做並沒有什么問題,但在幾天前我突然產生了一些想法:為什么非得有板有眼的彈出一個窗體給用戶呢,是不是可以在按鈕附近迅速呈現一個層來做這些事呢,類似快捷菜單那樣,用戶高興就在里面做一下該做的事,不高興就在其它地方點一下它就消失,本來很輕便快捷的操作,DUANG~彈出一個窗體來會不會令用戶心里咯噔一下呢,感受層面的事情往往是很微妙的,不管怎樣,我既然起了這個念頭,just try it。

我首先找了一下現成的方案,果然在牛逼的codeproject.com已經有牛人做了這樣的事情:

http://www.codeproject.com/Articles/17502/Simple-Popup-Control

簡單體驗了一下,的確是了不起的創造。原理是利用ToolStripControlHost可以承載自定義控件的這一能力,讓下拉式控件ToolStripDropDown將任何自定義控件像右鍵菜單那樣彈出來(別忘了右鍵菜單ContextMenuStrip就是繼承自ToolStripDropDown),這樣就等於把菜單作為一個容器,可以彈出任何或簡單或復雜的控件組合,同時又具有菜單具有的便捷性,召之即來揮之即去。當時了解到這方案的時候真挺開心,正是我想要的效果,感覺這下好了,不用瞎費勁自己造了。

但很快發現一個在我看來還挺在意的不足,就是ToolStripDropDown只有Show,沒有ShowDialog,就是不能以模式化(Modal,也有叫模態的,鑒於MSDN都稱模式,我也隨流叫它模式)的方式彈出,這是由ToolStripDropDown的固有能力決定的,該方案既然基於ToolStripDropDown,自然也受限於此,不能模式化彈出。這樣帶來的問題是某些情況下的調用體驗不好(體驗這種事當然不是用戶才有的專利,俺們碼農也是人,也要講體驗的說),比如彈出的控件是讓用戶輸入一些東西,完了用戶點擊某個按鈕什么的返回原窗體,然后在原窗體獲取用戶剛剛的輸入,然后接着做后面的事。由於非模式的Show不會阻塞代碼,所以就不能在Show的下方想當然的獲取值、使用值~這是顯然的。要想獲得值可能就得額外采取一些做法,例如響應彈出控件的關閉事件,或者把原窗體傳入彈出控件完了在后者中做原本應該在原窗體中做的事~等等,辦法當然有很多,但這都是因為只能Show帶來的多余的事,有什么比在一個方法中彈出控件、等待返回、繼續處理來的爽滑的呢,像這樣不是很自然嗎:

string s;
using (Popup p = new Popup())
{
    if (p.ShowDialog() != DialogResult.OK) { return; }

    s = p.InputText;
}
//go on
...

所以很遺憾,不得不揮別這個優秀的方案,造自己的輪子。不過受該方案的啟發,我想到用ContextMenu來做容器(注意這個菜單類跟上面提到的繼承自ToolStripDropDown的ContextMenuStrip大大的不同,前者是OS原生的菜單,就是在桌面、圖標以及文本框中右鍵彈出的那種菜單,.net是通過調API的方式來操作這樣的菜單,而后者則完全是.net實現,更多信息請參考MSDN,此處不展開),因為ContextMenu的Show是阻塞式的,正合我意。但一番嘗試之后放棄,它的菜單項MenuItem不像ToolStripItem那樣可以通過ToolStripControlHost承載自定義控件,希望是我能力有限,總之我做不到把自定義控件弄到ContextMenu上,也沒見過原生菜單上出現過文本框、復選框等奇怪的東西,如果您知道怎么擴展原生菜單,還望不吝賜教,先行謝過!

我還是打回.net的主意,當中仍然是做了許多不同的嘗試,Form、Panel、UserControl、ContainerControl、Control等等看起來適合做容器層的東西都試了個遍,甚至重新在ToolStripDropDown上打主意,最后選用Form,改造一番,自我感覺較理想的實現了我要的東西:一個叫做FloatLayerBase的基類,它本身繼承自System.Windows.Forms.Form類,而需要作為浮動層顯示的應用則繼承自FloatLayerBase進行實現,例如下面這個接受用戶輸入數值的NumInputDemo實現:

樣子和特點:

  • 不會令父窗口失去焦點(不會搶焦點的層才是好層):

    當然,男人不止一面:

    還有其它邊框樣式,有待用戶自行體驗,最后有demo提供。

  • 可以有調整尺寸的手柄

  • 可以點住客戶區拖動

別的一些應用:

這些都只是demo,沒那么好看和強大,重點是有了這個FloatLayerBase,就可以實現自己的浮動應用。

使用說明:

  1. 確保FloatLayerBase類在項目中~廢話。源碼在此:
    using System;
    using System.ComponentModel;
    using System.Drawing;
    using System.Runtime.InteropServices;
    using System.Windows.Forms;
    
    namespace AhDung.WinForm.Controls
    {
        /// <summary>
        /// 浮動層基類
        /// </summary>
        //Update:201508251451
        //- 將由OnShow中負責的首控件激活改為設TopMost=true實現,同時移除OnShow重寫
        //- 解決子控件無聚焦框(焦點虛線框,FocusCues)的問題
        //Update:201508261806
        //- 重繪右下角調整大小手柄,解決系統自繪在XP下太靠邊角從而覆蓋邊框的問題
        //- 支持邊緣和邊角拖動改變窗體大小
        //- 啟用雙緩沖
        public class FloatLayerBase : Form
        {
            /// <summary>
            /// 鼠標消息篩選器
            /// </summary>
            //由於本窗體為WS_CHILD,所以不會收到在窗體以外點擊鼠標的消息
            //該消息篩選器的作用就是讓本窗體獲知鼠標點擊情況,進而根據鼠標是否在本窗體以外的區域點擊,做出相應處理
            readonly AppMouseMessageHandler _mouseMsgFilter;
    
            /// <summary>
            /// 指示本窗體是否已ShowDialog過
            /// </summary>
            //由於多次ShowDialog會使OnLoad/OnShown重入,故需設置此標記以供重入時判斷
            bool _isShowDialogAgain;
    
            //邊框相關字段
            BorderStyle _borderType;
            Border3DStyle _border3DStyle;
            ButtonBorderStyle _borderSingleStyle;
            Color _borderColor;
            int _borderWidth;//邊框寬度,用於繪制SizeGrip時計算邊角偏移
    
            /// <summary>
            /// 獲取所繪制的邊框尺寸(邊框寬度x2)
            /// </summary>
            [Browsable(false)]
            public Size BorderSize
            {
                get { return new Size(_borderWidth, _borderWidth); }
            }
    
            /// <summary>
            /// 指示窗體是否處於可調整大小狀態
            /// </summary>
            [Browsable(false)]
            public bool CanReSize
            {
                get
                {
                    return this.SizeGripStyle == System.Windows.Forms.SizeGripStyle.Show
                    || (this.SizeGripStyle == System.Windows.Forms.SizeGripStyle.Auto && Modal);
                }
            }
    
            /// <summary>
            /// 獲取或設置邊框類型
            /// </summary>
            [Description("獲取或設置邊框類型。")]
            [DefaultValue(BorderStyle.Fixed3D)]
            public BorderStyle BorderType
            {
                get { return _borderType; }
                set
                {
                    if (_borderType == value) { return; }
                    _borderType = value;
                    this.UpdateBorderWidth();
                    Invalidate();
                }
            }
    
            /// <summary>
            /// 獲取或設置三維邊框樣式
            /// </summary>
            [Description("獲取或設置三維邊框樣式。")]
            [DefaultValue(Border3DStyle.RaisedInner)]
            public Border3DStyle Border3DStyle
            {
                get { return _border3DStyle; }
                set
                {
                    if (_border3DStyle == value) { return; }
                    _border3DStyle = value;
                    this.UpdateBorderWidth();
                    Invalidate();
                }
            }
    
            /// <summary>
            /// 獲取或設置線型邊框樣式
            /// </summary>
            [Description("獲取或設置線型邊框樣式。")]
            [DefaultValue(ButtonBorderStyle.Solid)]
            public ButtonBorderStyle BorderSingleStyle
            {
                get { return _borderSingleStyle; }
                set
                {
                    if (_borderSingleStyle == value) { return; }
                    _borderSingleStyle = value;
                    this.UpdateBorderWidth();
                    Invalidate();
                }
            }
    
            /// <summary>
            /// 獲取或設置邊框顏色(僅當邊框類型為線型時有效)
            /// </summary>
            [Description("獲取或設置邊框顏色(僅當邊框類型為線型時有效)。")]
            [DefaultValue(typeof(Color), "DarkGray")]
            public Color BorderColor
            {
                get { return _borderColor; }
                set
                {
                    if (_borderColor == value) { return; }
                    _borderColor = value;
                    Invalidate();
                }
            }
    
            protected override sealed CreateParams CreateParams
            {
                get
                {
                    CreateParams prms = base.CreateParams;
    
                    //prms.Style = 0;
                    //prms.Style |= -2147483648;   //WS_POPUP
                    prms.Style |= 0x40000000;      //WS_CHILD  重要,只有CHILD窗體才不會搶父窗體焦點
                    prms.Style |= 0x4000000;       //WS_CLIPSIBLINGS
                    prms.Style |= 0x10000;         //WS_TABSTOP
                    prms.Style &= ~0x40000;        //WS_SIZEBOX       去除
                    prms.Style &= ~0x800000;       //WS_BORDER        去除
                    prms.Style &= ~0x400000;       //WS_DLGFRAME      去除
                    //prms.Style &= ~0x20000;      //WS_MINIMIZEBOX   去除
                    //prms.Style &= ~0x10000;      //WS_MAXIMIZEBOX   去除
    
                    prms.ExStyle = 0;
                    //prms.ExStyle |= 0x1;         //WS_EX_DLGMODALFRAME 立體邊框
                    //prms.ExStyle |= 0x8;         //WS_EX_TOPMOST
                    prms.ExStyle |= 0x10000;       //WS_EX_CONTROLPARENT
                    //prms.ExStyle |= 0x80;        //WS_EX_TOOLWINDOW
                    //prms.ExStyle |= 0x100;       //WS_EX_WINDOWEDGE
                    //prms.ExStyle |= 0x8000000;   //WS_EX_NOACTIVATE
                    //prms.ExStyle |= 0x4;         //WS_EX_NOPARENTNOTIFY
    
                    return prms;
                }
            }
    
            //構造函數
            public FloatLayerBase()
            {
                //初始化消息篩選器。添加和移除在顯示/隱藏時負責
                _mouseMsgFilter = new AppMouseMessageHandler(this);
    
                this.DoubleBuffered = true;
    
                //初始化基類屬性
                InitBaseProperties();
    
                //初始化邊框相關
                _borderType = BorderStyle.Fixed3D;
                _border3DStyle = System.Windows.Forms.Border3DStyle.RaisedInner;
                _borderSingleStyle = ButtonBorderStyle.Solid;
                _borderColor = Color.DarkGray;
                this.UpdateBorderWidth();
            }
    
            protected override void OnLoad(EventArgs e)
            {
                //防止重入
                if (_isShowDialogAgain) { return; }
    
                //為首次ShowDialog設標記
                if (Modal) { _isShowDialogAgain = true; }
    
                //需得減掉兩層邊框寬度,運行時尺寸才與設計時完全相符,原因不明
                //確定與ControlBox、FormBorderStyle有關,但具體聯系不明
                if (!DesignMode)
                {
                    Size size = SystemInformation.FrameBorderSize;
                    this.Size -= size + size;//不可以用ClientSize,后者會根據窗口風格重新調整Size
                }
                base.OnLoad(e);
            }
    
            protected override void WndProc(ref Message m)
            {
                //當本窗體作為ShowDialog彈出時,在收到WM_SHOWWINDOW前,Owner會被Disable
                //故需在收到該消息后立即Enable它,不然Owner窗體和本窗體都將處於無響應狀態
                if (m.Msg == 0x18 && m.WParam != IntPtr.Zero && m.LParam == IntPtr.Zero
                    && Modal && Owner != null && !Owner.IsDisposed)
                {
                    if (Owner.IsMdiChild)
                    {
                        //當Owner是MDI子窗體時,被Disable的是MDI主窗體
                        //並且Parent也會指向MDI主窗體,故需改回為Owner,這樣彈出窗體的Location才會相對於Owner而非MDIParent
                        NativeMethods.EnableWindow(Owner.MdiParent.Handle, true);
                        NativeMethods.SetParent(this.Handle, Owner.Handle);//只能用API設置Parent,因為模式窗體是TopLevel,.Net拒絕為頂級窗體設置Parent
                    }
                    else
                    {
                        NativeMethods.EnableWindow(Owner.Handle, true);
                    }
                }
                else if (m.Msg == 0x84 && this.CanReSize)//WM_NCHITTEST。實現邊緣和邊角拖動改變窗體大小
                {
                    Point pt = this.PointToClient(NativeMethods.MakePoint(m.LParam));
                    Size size = this.ClientSize;
                    if (new Rectangle(0, 0, 5, 5).Contains(pt))
                    {
                        m.Result = (IntPtr)13;//HTTOPLEFT
                        return;
                    }
                    if (new Rectangle(5, 0, size.Width - 10, 3).Contains(pt))
                    {
                        m.Result = (IntPtr)12;//HTTOP
                        return;
                    }
                    if (new Rectangle(size.Width - 5, 0, 5, 5).Contains(pt))
                    {
                        m.Result = (IntPtr)14;//HTTOPRIGHT
                        return;
                    }
                    if (new Rectangle(size.Width - 3, 5, 3, size.Height - 5 - 16).Contains(pt))
                    {
                        m.Result = (IntPtr)11;//HTRIGHT
                        return;
                    }
                    if (new Rectangle(5, size.Height - 3, size.Width - 5 - 16, 3).Contains(pt))
                    {
                        m.Result = (IntPtr)15;//HTBOTTOM
                        return;
                    }
                    if (new Rectangle(0, size.Height - 5, 5, 5).Contains(pt))
                    {
                        m.Result = (IntPtr)16;//HTBOTTOMLEFT
                        return;
                    }
                    if (new Rectangle(0, 5, 3, size.Height - 10).Contains(pt))
                    {
                        m.Result = (IntPtr)10;//HTLEFT
                        return;
                    }
                }
                base.WndProc(ref m);
            }
    
            //畫邊框
            protected override void OnPaintBackground(PaintEventArgs e)
            {
                base.OnPaintBackground(e);
    
                if (_borderType == BorderStyle.Fixed3D)//繪制3D邊框
                {
                    ControlPaint.DrawBorder3D(e.Graphics, ClientRectangle, Border3DStyle);
                }
                else if (_borderType == BorderStyle.FixedSingle)//繪制線型邊框
                {
                    ControlPaint.DrawBorder(e.Graphics, ClientRectangle, BorderColor, BorderSingleStyle);
                }
            }
    
            protected override void OnPaint(PaintEventArgs e)
            {
                if (this.CanReSize)
                {
                    Size clientSize = this.ClientSize;
                    Rectangle rect = new Rectangle(clientSize.Width - 16, clientSize.Height - 16, 16, 16);
    
                    //畫手柄
                    DrawSizeGrip(e.Graphics, new Rectangle(rect.Location - BorderSize - new Size(1, 1), rect.Size));
    
                    //刨掉SizeGrip區域,防止基類再畫
                    e.Graphics.SetClip(rect, System.Drawing.Drawing2D.CombineMode.Exclude);
                }
                base.OnPaint(e);
                e.Graphics.ResetClip();
            }
    
            /// <summary>
            /// 繪制SizeGrip(調整大小的手柄),子類可重寫
            /// </summary>
            /// <param name="g">繪制器</param>
            /// <param name="rect">建議作圖區域</param>
            protected virtual void DrawSizeGrip(Graphics g, Rectangle rect)
            {
                Color backColor = this.BackColor;
                Brush color1 = new SolidBrush(ControlPaint.Dark(backColor));
                Brush color2 = new SolidBrush(ControlPaint.Dark(backColor, -0.5F));
                Brush color3 = new SolidBrush(ControlPaint.Dark(backColor, -0.1F));
                Brush color4 = new SolidBrush(ControlPaint.Light(backColor));
                Point pt = new Point(rect.X + 5, rect.Y + 5);//左上角偏移
    
                for (int i = 0; i < 4; i++)
                {
                    for (int j = 0; j < 4; j++)
                    {
                        if (j >= 3 - i)
                        {
                            g.FillRectangle(color1, new Rectangle(pt.X + j * 3, pt.Y + i * 3, 1, 1));
                            g.FillRectangle(color2, new Rectangle(pt.X + j * 3 + 1, pt.Y + i * 3, 1, 1));
                            g.FillRectangle(color3, new Rectangle(pt.X + j * 3, pt.Y + i * 3 + 1, 1, 1));
                            g.FillRectangle(color4, new Rectangle(pt.X + j * 3 + 1, pt.Y + i * 3 + 1, 1, 1));
                        }
                    }
                }
            }
    
            protected override void OnVisibleChanged(EventArgs e)
            {
                if (!DesignMode)
                {
                    if (Visible)
                    {
                        //使焦點子控件擁有聚焦框,重寫ShowFocusCues較麻煩
                        NativeMethods.SendMessage(this.Handle, 0x127/*WM_CHANGEUISTATE*/, (IntPtr)0x10002/*UISF_HIDEFOCUS | UIS_CLEAR*/, IntPtr.Zero);
                        NativeMethods.SendMessage(this.Handle, 0x128/*WM_UPDATEUISTATE*/, (IntPtr)0x10002/*UISF_HIDEFOCUS | UIS_CLEAR*/, IntPtr.Zero);
    
                        //顯示后添加鼠標消息篩選器以開始捕捉
                        Application.AddMessageFilter(_mouseMsgFilter);
                    }
                    else
                    {
                        //隱藏時則移除篩選器。之所以不放Dispose中是想盡早移除篩選器
                        Application.RemoveMessageFilter(_mouseMsgFilter);
                    }
                }
                base.OnVisibleChanged(e);
            }
    
            //實現窗體客戶區拖動
            //在WndProc中實現這個較麻煩,所以放到這里做
            protected override void OnMouseDown(MouseEventArgs e)
            {
                //讓鼠標點擊客戶區時達到與點擊標題欄一樣的效果,以此實現客戶區拖動
                NativeMethods.ReleaseCapture();
                NativeMethods.SendMessage(Handle, 0xA1/*WM_NCLBUTTONDOWN*/, (IntPtr)2/*CAPTION*/, IntPtr.Zero);
    
                base.OnMouseDown(e);
            }
    
            /// <summary>
            /// 顯示為模式窗體
            /// </summary>
            /// <param name="control">顯示在該控件下方</param>
            public DialogResult ShowDialog(Control control)
            {
                return ShowDialog(control, 0, control.Height);
            }
    
            /// <summary>
            /// 顯示為模式窗體
            /// </summary>
            /// <param name="control">觸發彈出窗體的控件</param>
            /// <param name="offsetX">相對control水平偏移</param>
            /// <param name="offsetY">相對control垂直偏移</param>
            public DialogResult ShowDialog(Control control, int offsetX, int offsetY)
            {
                return ShowDialog(control, new Point(offsetX, offsetY));
            }
    
            /// <summary>
            /// 顯示為模式窗體
            /// </summary>
            /// <param name="control">觸發彈出窗體的控件</param>
            /// <param name="offset">相對control偏移</param>
            public DialogResult ShowDialog(Control control, Point offset)
            {
                return this.ShowDialogInternal(control, offset);
            }
    
            /// <summary>
            /// 顯示為模式窗體
            /// </summary>
            /// <param name="item">顯示在該工具欄項的下方</param>
            public DialogResult ShowDialog(ToolStripItem item)
            {
                return ShowDialog(item, 0, item.Height);
            }
    
            /// <summary>
            /// 顯示為模式窗體
            /// </summary>
            /// <param name="item">觸發彈出窗體的工具欄項</param>
            /// <param name="offsetX">相對item水平偏移</param>
            /// <param name="offsetY">相對item垂直偏移</param>
            public DialogResult ShowDialog(ToolStripItem item, int offsetX, int offsetY)
            {
                return ShowDialog(item, new Point(offsetX, offsetY));
            }
    
            /// <summary>
            /// 顯示為模式窗體
            /// </summary>
            /// <param name="item">觸發彈出窗體的工具欄項</param>
            /// <param name="offset">相對item偏移</param>
            public DialogResult ShowDialog(ToolStripItem item, Point offset)
            {
                return this.ShowDialogInternal(item, offset);
            }
    
            /// <summary>
            /// 顯示窗體
            /// </summary>
            /// <param name="control">顯示在該控件下方</param>
            public void Show(Control control)
            {
                Show(control, 0, control.Height);
            }
    
            /// <summary>
            /// 顯示窗體
            /// </summary>
            /// <param name="control">觸發彈出窗體的控件</param>
            /// <param name="offsetX">相對control水平偏移</param>
            /// <param name="offsetY">相對control垂直偏移</param>
            public void Show(Control control, int offsetX, int offsetY)
            {
                Show(control, new Point(offsetX, offsetY));
            }
    
            /// <summary>
            /// 顯示窗體
            /// </summary>
            /// <param name="control">觸發彈出窗體的控件</param>
            /// <param name="offset">相對control偏移</param>
            public void Show(Control control, Point offset)
            {
                this.ShowInternal(control, offset);
            }
    
            /// <summary>
            /// 顯示窗體
            /// </summary>
            /// <param name="item">顯示在該工具欄下方</param>
            public void Show(ToolStripItem item)
            {
                Show(item, 0, item.Height);
            }
    
            /// <summary>
            /// 顯示窗體
            /// </summary>
            /// <param name="item">觸發彈出窗體的工具欄項</param>
            /// <param name="offsetX">相對item水平偏移</param>
            /// <param name="offsetY">相對item垂直偏移</param>
            public void Show(ToolStripItem item, int offsetX, int offsetY)
            {
                Show(item, new Point(offsetX, offsetY));
            }
    
            /// <summary>
            /// 顯示窗體
            /// </summary>
            /// <param name="item">觸發彈出窗體的工具欄項</param>
            /// <param name="offset">相對item偏移</param>
            public void Show(ToolStripItem item, Point offset)
            {
                this.ShowInternal(item, offset);
            }
    
            /// <summary>
            /// ShowDialog內部方法
            /// </summary>
            private DialogResult ShowDialogInternal(Component controlOrItem, Point offset)
            {
                //快速連續彈出本窗體將有可能遇到尚未Hide的情況下再次彈出,這會引發異常,故需做處理
                if (this.Visible) { return System.Windows.Forms.DialogResult.None; }
    
                this.SetLocationAndOwner(controlOrItem, offset);
                return base.ShowDialog();
            }
    
            /// <summary>
            /// Show內部方法
            /// </summary>
            private void ShowInternal(Component controlOrItem, Point offset)
            {
                if (this.Visible) { return; }//原因見ShowDialogInternal
    
                this.SetLocationAndOwner(controlOrItem, offset);
                base.Show();
            }
    
            /// <summary>
            /// 設置坐標及所有者
            /// </summary>
            /// <param name="controlOrItem">控件或工具欄項</param>
            /// <param name="offset">相對偏移</param>
            private void SetLocationAndOwner(Component controlOrItem, Point offset)
            {
                Point pt = Point.Empty;
    
                if (controlOrItem is ToolStripItem)
                {
                    ToolStripItem item = (ToolStripItem)controlOrItem;
                    pt.Offset(item.Bounds.Location);
                    controlOrItem = item.Owner;
                }
    
                Control c = (Control)controlOrItem;
                pt.Offset(GetControlLocationInForm(c));
                pt.Offset(offset);
                this.Location = pt;
    
                //設置Owner屬性與Show[Dialog](Owner)有不同,當Owner是MDIChild時,后者會改Owner為MDIParent
                this.Owner = c.FindForm();
            }
    
            /// <summary>
            /// 獲取控件在窗體中的坐標
            /// </summary>
            private static Point GetControlLocationInForm(Control c)
            {
                Point pt = c.Location;
                while (!((c = c.Parent) is Form))
                {
                    pt.Offset(c.Location);
                }
                return pt;
            }
    
            /// <summary>
            /// 更新邊框寬度
            /// </summary>
            private void UpdateBorderWidth()
            {
                if (_borderType == BorderStyle.None)
                {
                    _borderWidth = 0;
                }
                else if (_borderType == BorderStyle.Fixed3D)
                {
                    if (_border3DStyle == System.Windows.Forms.Border3DStyle.Adjust) { _borderWidth = 0; }
                    else if (_border3DStyle == System.Windows.Forms.Border3DStyle.Flat) { _borderWidth = 1; }
                    else { _borderWidth = CountOneInBits((uint)_border3DStyle); }
                }
                else
                {
                    if (_borderSingleStyle == ButtonBorderStyle.None) { _borderWidth = 0; }
                    else if (_borderSingleStyle == ButtonBorderStyle.Outset) { _borderWidth = 2; }
                    else { _borderWidth = 1; }
                }
            }
    
            /// <summary>
            /// 統計二進制中1的個數
            /// </summary>
            private static int CountOneInBits(uint num)
            {
                int count = 0;
                while (num != 0)
                {
                    num &= num - 1;
                    count++;
                }
                return count;
            }
    
            #region 屏蔽對本類影響重大的基類方法和屬性
    
            /// <summary>
            /// 初始化部分基類屬性
            /// </summary>
            private void InitBaseProperties()
            {
                base.ControlBox = false;                           //重要
                //必須得是SizableToolWindow才能支持調整大小的同時,不受SystemInformation.MinWindowTrackSize的限制
                base.FormBorderStyle = System.Windows.Forms.FormBorderStyle.SizableToolWindow;
                base.Text = string.Empty;                          //重要
                base.HelpButton = false;
                base.Icon = null;
                base.IsMdiContainer = false;
                base.MaximizeBox = false;
                base.MinimizeBox = false;
                base.ShowIcon = false;
                base.ShowInTaskbar = false;
                base.StartPosition = FormStartPosition.Manual;     //重要
                base.TopMost = true; //使本窗體像普通窗體一樣顯示后自動激活首控件
                base.WindowState = FormWindowState.Normal;
            }
    
            //屏蔽原方法
            [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
            [Obsolete("請使用別的重載!", true)]
            public new DialogResult ShowDialog() { throw new NotImplementedException(); }
    
            [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
            [Obsolete("請使用別的重載!", true)]
            public new DialogResult ShowDialog(IWin32Window owner) { throw new NotImplementedException(); }
    
            [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
            [Obsolete("請使用別的重載!", true)]
            public new void Show() { throw new NotImplementedException(); }
    
            [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
            [Obsolete("請使用別的重載!", true)]
            public new void Show(IWin32Window owner) { throw new NotImplementedException(); }
    
            //屏蔽原屬性
            [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
            [Obsolete("禁用該屬性!", true)]
            public new bool ControlBox { get { return false; } set { } }
    
            [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
            [Obsolete("設置邊框請使用Border相關屬性!", true)]
            public new FormBorderStyle FormBorderStyle { get { return System.Windows.Forms.FormBorderStyle.SizableToolWindow; } set { } }
    
            [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
            [Obsolete("禁用該屬性!", true)]
            public override sealed string Text { get { return string.Empty; } set { } }
    
            [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
            [Obsolete("禁用該屬性!", true)]
            public new bool HelpButton { get { return false; } set { } }
    
            [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
            [Obsolete("禁用該屬性!", true)]
            public new Image Icon { get { return null; } set { } }
    
            [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
            [Obsolete("禁用該屬性!", true)]
            public new bool IsMdiContainer { get { return false; } set { } }
    
            [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
            [Obsolete("禁用該屬性!", true)]
            public new bool MaximizeBox { get { return false; } set { } }
    
            [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
            [Obsolete("禁用該屬性!", true)]
            public new bool MinimizeBox { get { return false; } set { } }
    
            [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
            [Obsolete("禁用該屬性!", true)]
            public new bool ShowIcon { get { return false; } set { } }
    
            [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
            [Obsolete("禁用該屬性!", true)]
            public new bool ShowInTaskbar { get { return false; } set { } }
    
            [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
            [Obsolete("禁用該屬性!", true)]
            public new FormStartPosition StartPosition { get { return FormStartPosition.Manual; } set { } }
    
            [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
            [Obsolete("禁用該屬性!", true)]
            public new bool TopMost { get { return true; } set { } }
    
            [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
            [Obsolete("禁用該屬性!", true)]
            public new FormWindowState WindowState { get { return FormWindowState.Normal; } set { } }
    
            #endregion
    
            /// <summary>
            /// 程序鼠標消息篩選器
            /// </summary>
            private class AppMouseMessageHandler : IMessageFilter
            {
                readonly FloatLayerBase _layerForm;
    
                public AppMouseMessageHandler(FloatLayerBase layerForm)
                {
                    _layerForm = layerForm;
                }
    
                public bool PreFilterMessage(ref Message m)
                {
                    //如果在本窗體以外點擊鼠標,隱藏本窗體
                    //若想在點擊標題欄、滾動條等非客戶區也要讓本窗體消失,取消0xA1的注釋即可
                    //本例是根據坐標判斷,亦可以改為根據句柄,但要考慮子孫控件
                    //之所以用API而不用Form.DesktopBounds是因為后者不可靠
                    if ((m.Msg == 0x201/*|| m.Msg==0xA1*/)
                        && _layerForm.Visible && !NativeMethods.GetWindowRect(_layerForm.Handle).Contains(MousePosition))
                    {
                        _layerForm.Hide();//之所以不Close是考慮應該由調用者負責銷毀
                    }
    
                    return false;
                }
            }
    
            /// <summary>
            /// API封裝類
            /// </summary>
            private static class NativeMethods
            {
                [DllImport("user32.dll")]
                [return: MarshalAs(UnmanagedType.Bool)]
                public static extern bool EnableWindow(IntPtr hWnd, bool bEnable);
    
                [DllImport("user32.dll", CharSet = CharSet.Auto)]
                public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
    
                [DllImport("user32.dll")]
                public static extern bool ReleaseCapture();
    
                [DllImport("user32.dll", SetLastError = true)]
                public static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
    
                [DllImport("user32.dll", SetLastError = true)]
                private static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect);
    
                [StructLayout(LayoutKind.Sequential)]
                private struct RECT
                {
                    public int left;
                    public int top;
                    public int right;
                    public int bottom;
    
                    public static explicit operator Rectangle(RECT rect)
                    {
                        return new Rectangle(rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top);
                    }
                }
    
                public static Rectangle GetWindowRect(IntPtr hwnd)
                {
                    RECT rect;
                    GetWindowRect(hwnd, out rect);
                    return (Rectangle)rect;
                }
    
                public static int LOWORD(IntPtr n)
                {
                    return ((int)n) & 0xFFFF;
                }
    
                public static int HIWORD(IntPtr n)
                {
                    return (((int)n) >> 16) & 0xFFFF;
                }
    
                public static Point MakePoint(IntPtr n)
                {
                    return new Point(LOWORD(n), HIWORD(n));
                }
            }
        }
    }
    FloatLayerBase.cs
  2. 新建繼承窗體,選擇繼承自FloatLayerBase類;也可以新建普通窗體,然后把基類由Form改為FloatLayerBase
  3. 在設計器和源碼中打造浮動應用
  4. 在需要的地方使用它。關於使用,先看一下FloatLayerBase的部分公開成員:
    //屬性
    public BorderStyle BorderType { get; set; }
    public Border3DStyle Border3DStyle { get; set; }
    public ButtonBorderStyle BorderSingleStyle { get; set; }
    public Color BorderColor { get; set; }
    
    //方法
    public void Show(Control control);
    public void Show(Control control, Point offset);
    public void Show(Control control, int offsetX, int offsetY);
    public void Show(ToolStripItem item);
    public void Show(ToolStripItem item, Point offset);
    public void Show(ToolStripItem item, int offsetX, int offsetY);
    public DialogResult ShowDialog(Control control);
    public DialogResult ShowDialog(Control control, Point offset);
    public DialogResult ShowDialog(Control control, int offsetX, int offsetY);
    public DialogResult ShowDialog(ToolStripItem item);
    public DialogResult ShowDialog(ToolStripItem item, Point offset);
    public DialogResult ShowDialog(ToolStripItem item, int offsetX, int offsetY);

    上面4個屬性都是跟邊框有關的,邊框總共有3種形態,三維、線型、無,由BorderType指定;當為三維形態時,由Border3DStyle指定具體樣式;為線型時,由BorderSingleStyle和BorderColor分別指定具體線型和顏色。原Form.FormBorderStyle屬性已被屏蔽,不允許子類訪問,還有若干原Form的屬性也已屏蔽,原因都在源碼里。另外,原Form.SizeGripStyle照常使用,是否允許調整浮動層大小就靠它了

    方法就說一下Show和ShowDialog,顯然分別是用來非模式化/模式化顯示浮動層的,兩者在調用角度的重大區別就是,前者不會阻塞代碼,后者則會,實際應用中根據情況選用。每個方法從參數又分Control和ToolStripItem兩類,都是代表從什么控件上彈出浮動層的意思,前者接受Button、TextBox等控件(不能傳入Form,后果會不愉快),后者接受工具欄上面的項目,例如ToolStripButton、ToolStripTextBox之類的。重載可以指定相對control或item的偏移位置,默認是在control/item的下方彈出浮動層。最后無論是Show還是ShowDialog彈出來的浮動層,都可以像右鍵菜單那樣通過在其它地方點鼠標使之消失,這里需要說明一下:

    • 鼠標只會點在本程序內的窗體中時,讓浮動層消失。點在程序外的窗口、桌面、任務欄這些則不會。為什么要這樣是因為要做到完全像右鍵菜單那樣對全局鼠標敏感,需要全局鈎子,這會增加代碼量(性能且不說,沒測過不妄言),而且我認為沒必要全局敏感
    • 浮動層消失是調用Hide方法,所以對於模式化打開的浮動層,會返回DialogResult.Cancel,這是.net對模式對話框的設計使然,模式對話框被Hide或Close時,就是返回Cancel。在此也提醒一下調用者,在使用模式對話框時,永遠考慮有返回Cancel這種情況,不限於本例,而是所有對話框

    原Show()/Show(IWin32Window)和ShowDialog()/ShowDialog(IWin32Window)已被屏蔽,原因見源碼。

其它:

編寫期間一直使用PopupFormBase作為類名,發布最后時刻才改為現在的FloatLayerBase,所以demo中可能尚有依據原名起名的子類、方法名等。

Demo下載:

http://pan.baidu.com/s/1mgnGPGc

里面有個Tester供您體驗。

 

-文畢-


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM