4per Asked:2020-10-02 08:30:17 +0000 UTC2020-10-02 08:30:17 +0000 UTC 2020-10-02 08:30:17 +0000 UTC 如何将 ToolStrip 菜单放在窗口的标题栏中?在 Visual Studio 2019 中是如何完成的 772 在 Visual Studio 2019 中,主菜单已移至历史上称为标题栏的位置。我怎样才能在 WinForms 中做同样的事情? 视觉工作室: 我想我已经明白它是如何完成的了。这根本不是一个标题,而是它的模仿。提供非本地窗口控制按钮。但在谷歌浏览器中,按钮是原生的.. 谷歌浏览器: 因此,使用了不同的方法来解决这个问题。不过也有开发平台,可能不是.net 然而,如何以一种或另一种方式实现标题菜单? c# 2 个回答 Voted Best Answer 4per 2020-10-14T15:43:28Z2020-10-14T15:43:28Z 让我们画一个形状,放下面板Dock = Top。它有两个 ToolStripEx。 /// <summary> /// Вылечена часть глюков ToolStrip с фокусом /// </summary> public class ToolStripEx : ToolStrip { protected override void WndProc(ref Message m) { base.WndProc(ref m); if (m.Msg == WinApi.WM_MOUSEACTIVATE && m.Result == (IntPtr)WinApi.MA_ACTIVATEANDEAT) { m.Result = (IntPtr)WinApi.MA_ACTIVATE; } } } 属性设置: 剩下GripStyle = Hidden, RenderMode = System, Dock = Fill 正确的GripStyle = Hidden, RenderMode = System, Dock = Fill, LayoutStyle = Flow 在左侧,我们将放置: 按钮模仿窗口的系统菜单, 我们感兴趣的菜单本身的按钮, ToolStripLabel,它将作为窗口的标题 在右侧,我们抛出将模仿常规 ControlBox 的按钮。结果是这样的。 现在我们实际上需要删除标题,并为模拟器提供类似的行为。 最简单的方法是按下 ControlBox 并双击“虚拟标题”。 private void toolStripTitle_DoubleClick(object sender, EventArgs e) { WindowState = WindowState == FormWindowState.Maximized ? FormWindowState.Normal : FormWindowState.Maximized; UpdateControlBox(); } private void toolStripButtonClose_Click(object sender, EventArgs e) { Close(); } private void toolStripButtonMaximize_Click(object sender, EventArgs e) { WindowState = FormWindowState.Maximized; UpdateControlBox(); } private void toolStripButtonMinimize_Click(object sender, EventArgs e) { this.WindowState = FormWindowState.Minimized; } private void toolStripButtonRestore_Click(object sender, EventArgs e) { WindowState = FormWindowState.Normal; UpdateControlBox(); } private void UpdateControlBox() { toolStripButtonMaximize.Visible = WindowState != FormWindowState.Maximized; toolStripButtonRestore.Visible = WindowState != FormWindowState.Normal; } 隐藏本机标头的方法有三四种。但是除了一个之外,其他的都有一个缺点:我们失去了在任务栏中显示文本的能力,或者我们失去了调用“窗口系统菜单”的能力。 我的方法基于以下:在设计器中我们保存窗口的标准视图,然后在 Form.Load 处理程序中我们捕获 Handle“窗口系统菜单”,然后才在帮助下SetWindowLongPtr消除常规标题。 private IntPtr sysMenuHandle; private void Form1_Load(object sender, EventArgs e) { //Захватываем меню sysMenuHandle = WinApi.GetSystemMenu(this.Handle, false); //Ликвидируем штатный заголовок int style = WinApi.GetWindowLongPtr(this.Handle, WinApi.GWL_STYLE).ToInt32(); WinApi.SetWindowLongPtr(new HandleRef(this, this.Handle), WinApi.GWL_STYLE, (IntPtr)(style & ~WinApi.WS_CAPTION)); //Сворачиваем окно и возвращаем как-было //Это костыль, чтобы нормально перерисовалось окно после предыдущей операции //Не помешало бы найти менее корявый способ var winstate = this.WindowState; this.WindowState = FormWindowState.Minimized; this.WindowState = winstate; //Обновляем внешний вид ControlBox UpdateControlBox(); } 现在我们可以调用“窗口系统菜单” private void toolStripTitle_MouseDown(object sender, MouseEventArgs e) { if (e.Button == MouseButtons.Right) { IntPtr hWnd = this.Handle; WinApi.GetWindowRect(hWnd, out WinApi.RECT pos); int cmd = WinApi.TrackPopupMenu(sysMenuHandle, 0x100, this.Left + e.X, this.Top + e.Y, 0, hWnd, IntPtr.Zero); if (cmd > 0) WinApi.SendMessage(hWnd, 0x112, (IntPtr)cmd, IntPtr.Zero); } } private void toolStripIcon_MouseDown(object sender, MouseEventArgs e) { toolStripTitle_MouseDown(sender, e); } 它仍然返回按标题拖动窗口的能力 public partial class Form1 : Form, IMessageFilter { private HashSet<Control> controlsToMove = new HashSet<Control>(); public Form1() { InitializeComponent(); Application.AddMessageFilter(this); controlsToMove.Add(this.toolStripTitle); } public bool PreFilterMessage(ref Message m) { if (m.Msg == WinApi.WM_LBUTTONDOWN && controlsToMove.Contains(Control.FromHandle(m.HWnd))) { WinApi.SendMessage(this.Handle, WinApi.WM_NCLBUTTONDOWN, (IntPtr)WinApi.HT_CAPTION, (IntPtr)0); } return false; } 此解决方案中使用的一些答案: https ://stackoverflow.com/questions/23966253/moving-form-without-title-bar https://stackoverflow.com/questions/472301/toolstrip-sometimes-not-responding-to-鼠标单击 https://stackoverflow.com/questions/5245498/application-title-in-taskbar-but-not-titlebar https://stackoverflow.com/questions/21825352/how-to-open-window-右键单击系统菜单 https://stackoverflow.com/questions/16695154/winapi-getsystemmenu-without-ws-sysmenu-in-style MSDN.WhiteKnight 2020-10-15T14:36:27Z2020-10-15T14:36:27Z 最有可能的是,Chrome 使用此处描述的 DWM API 方法:使用 DWM 的自定义窗口框架。底线是通过 DwmExtendFrameIntoClientArea 将窗口的客户区扩展到其框架,并完成消息处理,使标准的最小化-关闭按钮继续工作。在 WinForms 中,可以这样应用: using System; using System.IO; using System.Collections.Generic; using System.Text; using System.Drawing; using System.Windows.Forms; using System.Runtime.InteropServices; namespace WindowsFormsApp1 { public partial class Form1 : Form { //размеры отступов клиентской области const int TOPEXTENDWIDTH = 1; const int BOTTOMEXTENDWIDTH = 30; const int LEFTEXTENDWIDTH = 1; const int RIGHTEXTENDWIDTH = 1; //WinAPI [StructLayout(LayoutKind.Sequential)] public struct MARGINS { public int cxLeftWidth; public int cxRightWidth; public int cyBottomHeight; public int cyTopHeight; } [StructLayout(LayoutKind.Sequential)] public struct RECT { public int left, top, right, bottom; } [DllImport("dwmapi.dll")] static extern int DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS margins); [DllImport("user32.dll", SetLastError = true)] static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect); [DllImport("user32.dll")] static extern bool AdjustWindowRectEx(ref RECT lpRect, uint dwStyle, bool bMenu, uint dwExStyle); [DllImport("dwmapi.dll")] static extern bool DwmDefWindowProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam, ref IntPtr plResult); static int GET_X_LPARAM(IntPtr lp) { short loword = (short)((ulong)lp & 0xffff); return loword; } static int GET_Y_LPARAM(IntPtr lp) { short hiword = (short)((((ulong)lp)>>16) & 0xffff); return hiword; } const uint WM_NCCALCSIZE = 0x0083; const uint WM_NCHITTEST = 0x0084; const uint WS_OVERLAPPED = 0x00000000; const uint WS_CAPTION = 0x00C00000; const uint WS_SYSMENU = 0x00080000; const uint WS_THICKFRAME = 0x00040000; const uint WS_MINIMIZEBOX = 0x00020000; const uint WS_MAXIMIZEBOX = 0x00010000; const uint WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX; const uint HTTOPLEFT = 13; const uint HTTOPRIGHT = 14; const uint HTTOP = 12; const uint HTCAPTION = 2; const uint HTLEFT = 10; const uint HTNOWHERE = 0; const uint HTRIGHT = 11; const uint HTBOTTOM = 15; const uint HTBOTTOMLEFT = 16; const uint HTBOTTOMRIGHT = 17; //обработка координат мыши для неклиентской области static IntPtr HitTestNCA(IntPtr hWnd, IntPtr wParam, IntPtr lParam) { // Get the point coordinates for the hit test. var ptMouse = new Point(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); // Get the window rectangle. RECT rcWindow; GetWindowRect(hWnd, out rcWindow); // Get the frame rectangle, adjusted for the style without a caption. RECT rcFrame = new RECT(); AdjustWindowRectEx(ref rcFrame, WS_OVERLAPPEDWINDOW & ~WS_CAPTION, false, 0); // Determine if the hit test is for resizing. Default middle (1,1). ushort uRow = 1; ushort uCol = 1; bool fOnResizeBorder = false; // Determine if the point is at the top or bottom of the window. if (ptMouse.Y >= rcWindow.top && ptMouse.Y < rcWindow.top + BOTTOMEXTENDWIDTH) { fOnResizeBorder = (ptMouse.Y < (rcWindow.top - rcFrame.top)); uRow = 0; } else if (ptMouse.Y < rcWindow.bottom && ptMouse.Y >= rcWindow.bottom - TOPEXTENDWIDTH) { uRow = 2; } // Determine if the point is at the left or right of the window. if (ptMouse.X >= rcWindow.left && ptMouse.X < rcWindow.left + LEFTEXTENDWIDTH) { uCol = 0; // left side } else if (ptMouse.X < rcWindow.right && ptMouse.X >= rcWindow.right - RIGHTEXTENDWIDTH) { uCol = 2; // right side } // Hit test (HTTOPLEFT, ... HTBOTTOMRIGHT) IntPtr[,] hitTests = new IntPtr[,] { { (IntPtr)HTTOPLEFT, fOnResizeBorder? (IntPtr)HTTOP : (IntPtr)HTCAPTION, (IntPtr)HTTOPRIGHT }, { (IntPtr)HTLEFT, (IntPtr)HTNOWHERE, (IntPtr)HTRIGHT}, { (IntPtr)HTBOTTOMLEFT, (IntPtr)HTBOTTOM, (IntPtr)HTBOTTOMRIGHT }, }; return hitTests[uRow, uCol]; } public Form1() { InitializeComponent(); foreach (ToolStripMenuItem item in menuStrip1.Items) { item.Paint += Item_Paint; } } private void Item_Paint(object sender, PaintEventArgs e) { var item = sender as ToolStripMenuItem; if (item == null) return; //для элементов, которые лежат на бывшей рамке окна, нужна нестандартная отрисовка e.Graphics.FillRectangle(SystemBrushes.Control, 2, 2, item.Width - 4,item.Height - 4); e.Graphics.DrawString(item.Text, SystemFonts.DefaultFont, SystemBrushes.ControlText, 2, 2); } //при первой активации окна расширим клиентскую область на рамку окна bool dwminit = false; private void Form1_Activated(object sender, EventArgs e) { if (dwminit == false) { // Extend the frame into the client area. MARGINS margins = new MARGINS(); margins.cxLeftWidth = LEFTEXTENDWIDTH; margins.cxRightWidth = RIGHTEXTENDWIDTH; margins.cyBottomHeight = BOTTOMEXTENDWIDTH; margins.cyTopHeight = TOPEXTENDWIDTH; int hr = DwmExtendFrameIntoClientArea(this.Handle, ref margins); dwminit = true; if (hr != 0) { throw Marshal.GetExceptionForHR(hr); } } } protected override void WndProc(ref Message m) { bool fCallDWP = true; IntPtr lRet = IntPtr.Zero; fCallDWP = !DwmDefWindowProc(m.HWnd, m.Msg, m.WParam, m.LParam, ref lRet); if (m.Msg == WM_NCCALCSIZE) { if (m.WParam != (IntPtr)0) { //убираем страндартную рамку lRet = IntPtr.Zero; fCallDWP = false; } } if (m.Msg == WM_NCHITTEST && lRet==IntPtr.Zero) { //обработка нажатий мыши lRet = HitTestNCA(m.HWnd, m.WParam, m.LParam); if (lRet != (IntPtr)HTNOWHERE) { fCallDWP = false; } } m.Result = lRet; //если сообщение не обработано, передаем в базовый класс if (fCallDWP) base.WndProc(ref m); } private void Form1_Paint(object sender, PaintEventArgs e) { //это нужно, чтобы были видны рамка и кнопки свернуть-закрыть в Windows 10, //так как рамку система принудительно заливает белым e.Graphics.FillRectangle(Brushes.Black, 0, 0, this.Width, BOTTOMEXTENDWIDTH); e.Graphics.FillRectangle(Brushes.Black, this.Width - RIGHTEXTENDWIDTH, 0, RIGHTEXTENDWIDTH, this.Height); e.Graphics.FillRectangle(Brushes.Black, 0, 0, LEFTEXTENDWIDTH, this.Height); e.Graphics.FillRectangle(Brushes.Black, 0, this.Height- TOPEXTENDWIDTH, this.Width, TOPEXTENDWIDTH); } private void Form1_Resize(object sender, EventArgs e) { //необходимо для корректной перерисовки рамки при изменении размера this.Invalidate(); } //другие обработчики событий... } } 在表单设计器中查看: 启动视图: 当然,这是一个相当粗略的示例,针对标准的 Windows 10 主题量身定制 - 为了使其无论主题如何都能正常工作,建议不要设置常量缩进,而是通过GetSystemMetrics获取窗口参数。但我认为这个想法很明确。 在实践中,我认为WinForms的这种方法不是很方便,因为需要手动绘制所有落入之前非客户区的元素。 警告:在 Vista/7 上,此方法仅适用于启用 DWM 组合。在以前的版本中,它不会。
让我们画一个形状,放下面板
Dock = Top。它有两个 ToolStripEx。属性设置:
剩下
GripStyle = Hidden, RenderMode = System, Dock = Fill正确的
GripStyle = Hidden, RenderMode = System, Dock = Fill, LayoutStyle = Flow在左侧,我们将放置:
在右侧,我们抛出将模仿常规 ControlBox 的按钮。结果是这样的。
现在我们实际上需要删除标题,并为模拟器提供类似的行为。
最简单的方法是按下 ControlBox 并双击“虚拟标题”。
隐藏本机标头的方法有三四种。但是除了一个之外,其他的都有一个缺点:我们失去了在任务栏中显示文本的能力,或者我们失去了调用“窗口系统菜单”的能力。
我的方法基于以下:在设计器中我们保存窗口的标准视图,然后在 Form.Load 处理程序中我们捕获 Handle“窗口系统菜单”,然后才在帮助下
SetWindowLongPtr消除常规标题。现在我们可以调用“窗口系统菜单”
它仍然返回按标题拖动窗口的能力
此解决方案中使用的一些答案: https ://stackoverflow.com/questions/23966253/moving-form-without-title-bar https://stackoverflow.com/questions/472301/toolstrip-sometimes-not-responding-to-鼠标单击 https://stackoverflow.com/questions/5245498/application-title-in-taskbar-but-not-titlebar https://stackoverflow.com/questions/21825352/how-to-open-window-右键单击系统菜单 https://stackoverflow.com/questions/16695154/winapi-getsystemmenu-without-ws-sysmenu-in-style
最有可能的是,Chrome 使用此处描述的 DWM API 方法:使用 DWM 的自定义窗口框架。底线是通过 DwmExtendFrameIntoClientArea 将窗口的客户区扩展到其框架,并完成消息处理,使标准的最小化-关闭按钮继续工作。在 WinForms 中,可以这样应用:
在表单设计器中查看:
启动视图:
当然,这是一个相当粗略的示例,针对标准的 Windows 10 主题量身定制 - 为了使其无论主题如何都能正常工作,建议不要设置常量缩进,而是通过GetSystemMetrics获取窗口参数。但我认为这个想法很明确。
在实践中,我认为WinForms的这种方法不是很方便,因为需要手动绘制所有落入之前非客户区的元素。
警告:在 Vista/7 上,此方法仅适用于启用 DWM 组合。在以前的版本中,它不会。