Работа с неклиентской областью окна


Давно собирался написать о том, как сделать окно с элементами в неклиентской части. Такое, как, например, в 2007-ом офисе.
Работа с неклиентской областью окнаРабота с неклиентской областью окна

Идея помещения элементов в заголовок окна под 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);
И в случае необходимости вызывать DwmDefWindowProc
if (DwmApi.IsDwmEnabled())
{
    IntPtr result = IntPtr.Zero;
    DwmApi.DwmDefWindowProc(handle, msg, wParam, lParam, ref result);
    if (result != IntPtr.Zero)
    {
        return result;
    }
}
Это понятно, работает при включённом DWM. Если же DWM отключён, то можно пойти двумя путями:
Первый - создание некой небольшой неклиентской области по краям, которую перерисовывать средствами 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();
}
Обрабатывать сообщение WM_NCHITTEST, чтобы позиции неклиентских областей определялись там где надо:
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 &amp;&amp; ptMouse.Y < 50)
    {
        fOnResizeBorder = (ptMouse.Y < 0);
        uRow = 0;
    }
    else if (ptMouse.Y < mainGrid.ActualHeight + borderSize.Bottom &amp;&amp; ptMouse.Y >= mainGrid.ActualHeight)
    {
        uRow = 2;
    }

    if (ptMouse.X >= -borderSize.Left &amp;&amp; ptMouse.X < 0)
    {
        uCol = 0; 
    }
    else if (ptMouse.X < mainGrid.ActualWidth + borderSize.Right &amp;&amp; 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]);
}
И обрабатывать 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-а придётся использовать абсолютно другой способ рисования кнопок заголовка, и это не является удачным решением.

КОММЕНТАРИИ


НОВЫЙ КОММЕНТАРИЙ


*жирный*
_курсив_
+подчеркнутый+
! заголовок 1
!! заголовок 2
* список
** список 2
# нумерованый список
## нумерованый список 2
[url:http://www.example.com]
{"без форматирования"}
Полное описание синтаксиса