Работа с неклиентской областью окна
2 февраля 2008 - 19:52
Давно собирался написать о том, как сделать окно с элементами в неклиентской части. Такое, как, например, в 2007-ом офисе.
Идея помещения элементов в заголовок окна под Windows Vista неплохо описана здесь.
В общем-то все просто:
Достаточно перехватить сообщение WM_NCCALCSIZE, когда wParam = true и вернуть 0:
Расширить aero на нужную высоту:
И в случае необходимости вызывать DwmDefWindowProc
Это понятно, работает при включённом DWM. Если же DWM отключён, то можно пойти двумя путями:
Первый - создание некой небольшой неклиентской области по краям, которую перерисовывать средствами GDI, а для внутренней область(которая включает заголовок окна и большую круглую кнопку) уже использовать wpf.
Второй - не создавать неклиентской области, а все рисовать wpf-ом.
В данном примере я остановился на первом, так как думал, что при втором производительность будет хуже, но как показали дальнейшие эксперименты (о них я надеюсь написать как-нибудь потом) разницы не заметно, ибо наибольшее падание производительности вызывает SetWindowRgn, который используется в обоих случаях.
Осуществить первый вариант, достаточно просто:
Нужно установить у окна BorderStyle в None.
При каждом изменении размеров окна, использовать SetWindowRgn, для получения нужной формы окна:
Обрабатывать сообщение WM_NCHITTEST, чтобы позиции неклиентских областей определялись там где надо:
И обрабатывать WM_NCPAINT для перерисовки неклиентской области средствами GDI+
Скачать пример с исходниками:
Похожий результат (но похуже с точки зрения внешнего вида) можно получить ещё несколькими способами:
1. Использовать layered-окна и полностью копировать dwm. В WPF`е для этого достаточно включить у окна AllowTranparency в true. Но данный способ хорош только для небольших окон, которые не изменяют размеров, либо растягиваются только за крайний нижний угол, т.к. при изменении размеров происходит некрасивое моргание, плюс в Windows XP до SP3 были проблемы с производительностью layered-окон. Хотя я думаю, что как-нибудь опишу данный метод отдельно, т.к. он имеет право на жизнь.
2. Использовать описанный здесь метод. Идея данного метода в том, что создается дочернее прозрачное окно на которое кидаются кнопки и которое таскается вместе с главным. Подобным образом я делал окно в стиле Yahoo Messenger. Но в этом случае есть слегка заметное отставание дочернего окна (особенно на слабых компьютерах), при движении, плюс весьма забавные фокусы при сворачивании/разворачивании окна.
3. Делать окно с BorderStyle=None. Расширять DWM на высоту заголовка и создавать свои кнопки закрытия/сворачивания /разворачивания окна. Не очень красиво за счёт того, что кнопки выглядят нестандартно, но зато реализуется без особых трудозатрат.
4. Рисовать все в WM_NCPAINT`е. Это тоже вполне реально. Года полтора назад (когда висты ещё и не было) я писал такой прототип. Но в этом случае либо DWM не должен поддерживаться, что плохо, либо для DWM-а придётся использовать абсолютно другой способ рисования кнопок заголовка, и это не является удачным решением.
Идея помещения элементов в заголовок окна под Windows Vista неплохо описана здесь.
В общем-то все просто:
Достаточно перехватить сообщение WM_NCCALCSIZE, когда wParam = true и вернуть 0:
// Функия окна при включенном DWM private IntPtr DwmWindowProc( IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { switch (msg) { // Обработка вычисления размеров неклиентской области окна case WinApi.WM_NCCALCSIZE: { if (wParam.ToInt32() == 1) { handled = true; return IntPtr.Zero; } break; } } return IntPtr.Zero; }
Расширить aero на нужную высоту:
HwndSource mainWindowSrc = HwndSource.FromHwnd(handle); mainWindowSrc.CompositionTarget.BackgroundColor = Colors.Transparent; DwmApi.MARGINS margins = new DwmApi.MARGINS((int)sizers.Left, (int)sizers.Top + (int)titleBar.ActualHeight, (int)sizers.Right, (int)sizers.Bottom); DwmApi.DwmExtendFrameIntoClientArea(handle, margins);
if (DwmApi.IsDwmEnabled()) { IntPtr result = IntPtr.Zero; DwmApi.DwmDefWindowProc(handle, msg, wParam, lParam, ref result); if (result != IntPtr.Zero) { return result; } }
Первый - создание некой небольшой неклиентской области по краям, которую перерисовывать средствами GDI, а для внутренней область(которая включает заголовок окна и большую круглую кнопку) уже использовать wpf.
Второй - не создавать неклиентской области, а все рисовать wpf-ом.
В данном примере я остановился на первом, так как думал, что при втором производительность будет хуже, но как показали дальнейшие эксперименты (о них я надеюсь написать как-нибудь потом) разницы не заметно, ибо наибольшее падание производительности вызывает SetWindowRgn, который используется в обоих случаях.
Осуществить первый вариант, достаточно просто:
Нужно установить у окна BorderStyle в None.
При каждом изменении размеров окна, использовать SetWindowRgn, для получения нужной формы окна:
// Установка региона окна private void SetNonDwmRgn(double newWidth, double newHeight) { int topSide = (int)SystemParameters.ResizeFrameHorizontalBorderHeight-1; int bottomSide = (int)SystemParameters.ResizeFrameHorizontalBorderHeight-1; int leftSide = (int)SystemParameters.ResizeFrameVerticalBorderWidth-1; int rightSide = (int)SystemParameters.ResizeFrameVerticalBorderWidth-1; //WinApi.SetWindowRgn(handle, IntPtr.Zero, true); int width = (int)newWidth; int height = (int)newHeight; GraphicsPath path = new GraphicsPath(); if ((int)sizeBorder.CornerRadius.TopLeft!=0) path.AddArc(leftSide-1, topSide-1, (int)sizeBorder.CornerRadius.TopLeft*2, (int)sizeBorder.CornerRadius.TopLeft*2, 180, 90); path.AddLine((int)sizeBorder.CornerRadius.TopLeft, topSide, width - (int)sizeBorder.CornerRadius.TopRight - rightSide, topSide); if ((int)sizeBorder.CornerRadius.TopRight != 0) path.AddArc(width - (int)sizeBorder.CornerRadius.TopRight*2 - rightSide, topSide-1, (int)sizeBorder.CornerRadius.TopRight * 2, (int)sizeBorder.CornerRadius.TopRight * 2, 270, 90); path.AddLine(width - rightSide, (int)sizeBorder.CornerRadius.TopRight + topSide, width - rightSide, height - (int)sizeBorder.CornerRadius.BottomRight - bottomSide); if ((int)sizeBorder.CornerRadius.BottomRight != 0) path.AddArc(width - (int)sizeBorder.CornerRadius.BottomRight - rightSide-1, height - (int)sizeBorder.CornerRadius.BottomRight*2 - bottomSide-1, (int)sizeBorder.CornerRadius.BottomRight * 2, (int)sizeBorder.CornerRadius.BottomRight * 2, 0, 90); path.AddLine(width - (int)sizeBorder.CornerRadius.BottomRight - rightSide, height - bottomSide, (int)sizeBorder.CornerRadius.BottomLeft + leftSide, height - bottomSide); if ((int)sizeBorder.CornerRadius.BottomLeft != 0) path.AddArc(leftSide, height - (int)sizeBorder.CornerRadius.BottomLeft*2 - bottomSide-1, (int)sizeBorder.CornerRadius.BottomLeft * 2, (int)sizeBorder.CornerRadius.BottomLeft * 2, 90, 90); path.AddLine(leftSide, height - (int)sizeBorder.CornerRadius.BottomLeft - bottomSide, leftSide, (int)sizeBorder.CornerRadius.TopLeft + bottomSide-1); Region region = new Region(path); using (Graphics g = Graphics.FromHwnd(handle)) { WinApi.SetWindowRgn(handle, region.GetHrgn(g), true); } region.Dispose(); }
case WinApi.WM_NCHITTEST: { IntPtr ncHitTest = DoNcHitTest(msg, wParam, lParam); if (ncHitTest != IntPtr.Zero) { handled = true; return ncHitTest; } break; } private IntPtr DoNcHitTest(int msg, IntPtr wParam, IntPtr lParam) { int mp = lParam.ToInt32(); Point ptMouse = new Point((short)(mp & 0x0000FFFF), (short)((mp >> 16) & 0x0000FFFF)); ptMouse = mainGrid.PointFromScreen(ptMouse); IInputElement hitTested = mainGrid.InputHitTest(ptMouse); if ((hitTested != null) && (hitTested != mainGrid)) { if (hitTested == titleBar) return new IntPtr(WinApi.HTCAPTION); return IntPtr.Zero; } if (DwmApi.IsDwmEnabled()) { IntPtr result = IntPtr.Zero; DwmApi.DwmDefWindowProc(handle, msg, wParam, lParam, ref result); if (result != IntPtr.Zero) { return result; } } int uRow = 1; int uCol = 1; bool fOnResizeBorder = false; Thickness borderSize = DwmApi.IsDwmEnabled() ? sizers : new Thickness(sizers.Left + SystemParameters.ResizeFrameVerticalBorderWidth, sizers.Top + SystemParameters.ResizeFrameHorizontalBorderHeight, sizers.Right + SystemParameters.ResizeFrameVerticalBorderWidth, sizers.Bottom + SystemParameters.ResizeFrameHorizontalBorderHeight); if (ptMouse.Y >= -borderSize.Top && ptMouse.Y < 50) { fOnResizeBorder = (ptMouse.Y < 0); uRow = 0; } else if (ptMouse.Y < mainGrid.ActualHeight + borderSize.Bottom && ptMouse.Y >= mainGrid.ActualHeight) { uRow = 2; } if (ptMouse.X >= -borderSize.Left && ptMouse.X < 0) { uCol = 0; } else if (ptMouse.X < mainGrid.ActualWidth + borderSize.Right && ptMouse.X >= mainGrid.ActualWidth) { uCol = 2; } int[][] hitTests = new int[][] { new int[] {fOnResizeBorder ? WinApi.HTTOPLEFT:WinApi.HTLEFT, fOnResizeBorder ? WinApi.HTTOP : WinApi.HTCAPTION, fOnResizeBorder ? WinApi.HTTOPRIGHT:WinApi.HTRIGHT}, new int[] {WinApi.HTLEFT, WinApi.HTNOWHERE, WinApi.HTRIGHT}, new int[] {WinApi.HTBOTTOMLEFT, WinApi.HTBOTTOM, WinApi.HTBOTTOMRIGHT}, }; return new IntPtr(hitTests[uRow][uCol]); }
Скачать пример с исходниками:
Похожий результат (но похуже с точки зрения внешнего вида) можно получить ещё несколькими способами:
1. Использовать layered-окна и полностью копировать dwm. В WPF`е для этого достаточно включить у окна AllowTranparency в true. Но данный способ хорош только для небольших окон, которые не изменяют размеров, либо растягиваются только за крайний нижний угол, т.к. при изменении размеров происходит некрасивое моргание, плюс в Windows XP до SP3 были проблемы с производительностью layered-окон. Хотя я думаю, что как-нибудь опишу данный метод отдельно, т.к. он имеет право на жизнь.
2. Использовать описанный здесь метод. Идея данного метода в том, что создается дочернее прозрачное окно на которое кидаются кнопки и которое таскается вместе с главным. Подобным образом я делал окно в стиле Yahoo Messenger. Но в этом случае есть слегка заметное отставание дочернего окна (особенно на слабых компьютерах), при движении, плюс весьма забавные фокусы при сворачивании/разворачивании окна.
3. Делать окно с BorderStyle=None. Расширять DWM на высоту заголовка и создавать свои кнопки закрытия/сворачивания /разворачивания окна. Не очень красиво за счёт того, что кнопки выглядят нестандартно, но зато реализуется без особых трудозатрат.
4. Рисовать все в WM_NCPAINT`е. Это тоже вполне реально. Года полтора назад (когда висты ещё и не было) я писал такой прототип. Но в этом случае либо DWM не должен поддерживаться, что плохо, либо для DWM-а придётся использовать абсолютно другой способ рисования кнопок заголовка, и это не является удачным решением.