Создание кастомного стиля окна со стандартным поведением

Чтобы убрать стандартную рамку окна установим свойство Window.WindowStyle как None. Получим такое окно:

введите сюда описание изображения

Мы видим 1-пиксельную серую рамку, белую (фоновый цвет окна, как полагаю) полоску сверху и тень. Размеры окна можно изменять, а рамка изменения размера располагается за пределами видимой рамки окна (за исключением верхней границы, которая, как решила Microsoft, должна находится именно в пределах видимой рамки - не на тени).

Именно эту верхнюю границу я и хотел бы сделать невидимой, оставляя возможность для изменения размера окна сверху. Меня бы удовлетворило решение, позволяющее размещать мои собственные кнопки просто поверх этой рамки с краю.

Можно сделать окно AllowsTransparency, но тогда пропадёт тень, рамка изменения размера окна (по видимому пропадает вся неклиентская область, и окно полностью в нашем распоряжении) и анимации сворачивания/разворачивания. Можно, конечно, попробовать сделать это всё самому: создать собственную рамку изменения размера окна просто с помощью каких нибудь элементов (например, Rectangle), нарисовать тень под нею, забить на анимации сворачивания/разворачивания.

Но возникает очень неприятный эффект - окно сильно дёргается при изменении его размера за такую нестандартную рамку. Т.е. обычный вызов SetWindowPos для AllowsTransparency окна приводит к таким проблемам. Использование флага SWP_NOCOPYBITS проблему не решает (это значит не нужно предлагать создавать обработчик на WM_WINDOWPOSCHANGING (и на WM_NCCALCSIZE, на сколько знаю, аналогично) и что там делать). Обычно такое поведение связано с тем, что выданная приложению временная квота для обновления содержимого окна просто истекает, и сама Windows копирует то, что есть в верхний левый угол, а остальное пространство окна заполняет фоновым цветом (далее приложение само должно "догонять"). На Windows 7 с отключённым Aero проблема остаётся.

Значит вопрос в том, как разместить пользовательские элементы задевая стандартную рамку изменения размера окна сверху, либо устранить эффект от вызова SetWindowPos для такого стиля окна.

Почему WindowChrome не подходит: да, с использованием этого WindowChrome дёргание действительно пропадает, но при условии, что GlassFrameThickness не 0 и NonClientFrameEdges не None. Последнее добавляет такую же полоску с указываемой стороны. AllowsTransparency нельзя применять вместе с такими значениями свойств (у меня просто окно не разворачивалось с панели задач).

Проект строится под .NET 4.5.


Ответы (1 шт):

Автор решения: MSDN.WhiteKnight

Вы не сможете полностью избавиться от мерцания при изменении размера из-за ограничений DWM в новых версиях Windows. Собственно, наиболее известное приложение WPF, Visual Studio, тоже подвержено этой проблеме, по крайней мере по состоянию на VS 2017. Но наилучшего результата, по моим тестам, можно достичь, убрав неклиентскую область с помощью DwmExtendFrameIntoClientArea и добавив собственный обработчик сообщения WM_NCHITTEST:

<Window x:Class="WpfTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Height="250" Width="400"  Loaded="Window_Loaded_1" WindowStyle="SingleBorderWindow">

        <Grid Background="Black" x:Name="grid">

            <Button Content="Button1" HorizontalAlignment="Left" Height="50" Margin="64,0,0,0" VerticalAlignment="Top" 
               Background="Red" Width="150" Click="Button_Click"/>
            <Button Content="X" HorizontalAlignment="Right" Height="50" Margin="0,0,0,0" VerticalAlignment="Top" 
               Background="Green" Width="50" Click="Button_Click_1"/>
            <Label  Content="Label" HorizontalAlignment="Left"  Margin="0,0,0,0" Foreground="White"
                   VerticalAlignment="Top" Width="40" />

        </Grid>    
</Window>


using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
using System.Runtime.InteropServices;

namespace WpfTest
{
    public partial class MainWindow : Window
    {
        const int BORDERWIDTH = 5; 

        [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;

        IntPtr Handle;

        public MainWindow()
        {
            InitializeComponent();
        }

        //обработка координат мыши для неклиентской области
        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 + BORDERWIDTH)
            {
                fOnResizeBorder = (ptMouse.Y < (rcWindow.top - rcFrame.top));
                uRow = 0;
            }
            else if (ptMouse.Y < rcWindow.bottom && ptMouse.Y >= rcWindow.bottom - BORDERWIDTH)
            {
                uRow = 2;
            }

            // Determine if the point is at the left or right of the window.
            if (ptMouse.X >= rcWindow.left && ptMouse.X < rcWindow.left + BORDERWIDTH)
            {
                uCol = 0; // left side
            }
            else if (ptMouse.X < rcWindow.right && ptMouse.X >= rcWindow.right - BORDERWIDTH)
            {
                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];
        }

        //обработчик сообщений для окна
        private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            bool fCallDWP = true;
            IntPtr lRet = IntPtr.Zero;
            fCallDWP = !DwmDefWindowProc(hwnd, msg, wParam, lParam, ref lRet);

            if (msg == WM_NCCALCSIZE)
            {
                if (wParam != (IntPtr)0)
                {
                    //убираем страндартную рамку
                    lRet = IntPtr.Zero;
                    fCallDWP = false;
                }
            }

            if (msg == WM_NCHITTEST && lRet == IntPtr.Zero)
            {
                //обработка нажатий мыши
                lRet = HitTestNCA(hwnd, wParam, lParam);

                if (lRet != (IntPtr)HTNOWHERE)
                {
                    fCallDWP = false;
                }
            }

            //если сообщение не обработано, передаем базовой процедуре
            if (fCallDWP) handled = false;
            else handled = true;

            return lRet;
        }

        private void Window_Loaded_1(object sender, RoutedEventArgs e)
        {
            WindowInteropHelper h = new WindowInteropHelper(this);
            HwndSource source = HwndSource.FromHwnd(h.Handle);
            Handle = h.Handle;
            source.AddHook(new HwndSourceHook(WndProc));//регистрируем обработчик сообщений
            ExtendClientArea();
        }

        void ExtendClientArea()
        {
            // Extend the frame into the client area.
            MARGINS margins = new MARGINS();

            margins.cxLeftWidth = 0;
            margins.cxRightWidth = 0;
            margins.cyBottomHeight = 0;
            margins.cyTopHeight = 0;

            int hr = DwmExtendFrameIntoClientArea(this.Handle, ref margins);

            if (hr != 0)
            {
                throw Marshal.GetExceptionForHR(hr);
            }
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            MessageBox.Show("Button 1");
        }

        private void Button_Click_1(object sender, RoutedEventArgs e)
        {
            this.Close();
        }
    }
}

Выглядит так:

custom window

Протестировано на Windows 10 - выглядит получше, чем при использовании WindowChrome, при перетаскивании левого края само окно не мерцает (хотя все элементы, положение которых привязано к правому краю, все равно мерцают).


В Windows 7 способ тоже работает и дает неплохой результат, но с двумя поправками:

  • Стиль окна должен быть WindowStyle="None". При других стилях мерцание противоположного края возвращается
  • После активации/деактивации окна у него возвращается стандартная рамка, которая затем исчезает при первой перерисовке визуального дерева WPF. Для избавления от этого можно добавить обработчики событий:

    private void Window_Deactivated_1(object sender, EventArgs e)
    {
        grid.InvalidateVisual();
    }
    
    private void Window_Activated_1(object sender, EventArgs e)
    {
        grid.InvalidateVisual();
    }
    

где grid - корневой элемент Grid, в котором расположены другие элементы окна.

→ Ссылка