Страницы

Поиск по вопросам

воскресенье, 8 декабря 2019 г.

Как поместить меню ToolStrip в заголовок окна? Как в Visual Studio 2019 сделано

#c_sharp #winforms


В Visual Studio 2019 главное меню переехало в строку, которая исторически называется
термином "заголовок окна". Как мне на WinForms сделать так же?

Visual Studio:




Кажется, я уже понял как это сделано. Это не заголовок вовсе, а его имитация. Выдают
не родные кнопки управления окна. А вот в Google Chrome кнопки родные..

Google Chrome:



А значит, там использован другой подход к решению этого вопроса. Впрочем, там и платформа
разработки, наверняка не .net

И всё-таки как реализовать меню-заголовок тем или иным способом? 
    


Ответы

Ответ 1



Нарисуем форму, бросим панель Dock = Top. На неё два ToolStripEx. /// /// Вылечена часть глюков ToolStrip с фокусом /// 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; } Для того, чтобы скрыть родной заголовок, есть три или четыре способа. Но все, кроме одного, страдают недостатками: мы теряем возможность отображать текст в TaskBar, или теряем возможность вызывать "системное меню окна". Мой способ основан на следующем: в дизайнере мы сохраняем стандартный вид окна, затем в обработчике 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 controlsToMove = new HashSet(); 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-a-mouse-click https://stackoverflow.com/questions/5245498/application-title-in-taskbar-but-not-titlebar https://stackoverflow.com/questions/21825352/how-to-open-window-system-menu-on-right-click https://stackoverflow.com/questions/16695154/winapi-getsystemmenu-without-ws-sysmenu-in-style

Ответ 2



Скорее всего, Chrome использует способ с DWM API, описанный здесь: Custom Window Frame Using 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(); } //другие обработчики событий... } } Вид в Form Designer: Вид при запуске: Естественно, это довольно грубый пример, заточенный под стандартную тему Windows 10 - чтобы это нормально работало независимо от темы, желательно не задавать константные отступы, а получать параметры окна через GetSystemMetrics. Но я думаю, идея понятна. На практике этот способ для WinForms, я думаю, не очень удобен, из-за необходимости рисовать вручную все элементы, которые попадают в бывшую неклиентскую область. Внимание: На Vista/7 этот способ будет работать только с включенным DWM Composition. На предыдущих версиях - вообще не будет.

Комментариев нет:

Отправить комментарий