{"id":120,"date":"2020-01-01T05:48:04","date_gmt":"2020-01-01T05:48:04","guid":{"rendered":"https:\/\/engy.us\/blog\/?p=120"},"modified":"2021-11-26T16:05:28","modified_gmt":"2021-11-26T16:05:28","slug":"implementing-a-custom-window-title-bar-in-wpf","status":"publish","type":"post","link":"https:\/\/engy.us\/blog\/2020\/01\/01\/implementing-a-custom-window-title-bar-in-wpf\/","title":{"rendered":"Implementing a Custom Window Title Bar in WPF"},"content":{"rendered":"\n<p>There are several good reasons for wanting custom window chrome in WPF, such as fitting in additional UI or <a href=\"https:\/\/engy.us\/blog\/2018\/10\/20\/dark-theme-in-wpf\/\">implementing a Dark theme<\/a>. However the actual implementation is kind of tricky, since it is now your job to provide a bunch of features that you used to get for free. I&#8217;ll walk you through my implementation.<\/p>\n\n\n\n<h2>Appearance<\/h2>\n\n\n\n<p>I chose to emulate the Windows 10 style.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img data-attachment-id=\"121\" data-permalink=\"https:\/\/engy.us\/blog\/2020\/01\/01\/implementing-a-custom-window-title-bar-in-wpf\/win10titlebar\/\" data-orig-file=\"https:\/\/i2.wp.com\/engy.us\/blog\/wp-content\/uploads\/2020\/01\/Win10TitleBar.png?fit=360%2C43&amp;ssl=1\" data-orig-size=\"360,43\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"Win10TitleBar\" data-image-description=\"\" data-medium-file=\"https:\/\/i2.wp.com\/engy.us\/blog\/wp-content\/uploads\/2020\/01\/Win10TitleBar.png?fit=300%2C36&amp;ssl=1\" data-large-file=\"https:\/\/i2.wp.com\/engy.us\/blog\/wp-content\/uploads\/2020\/01\/Win10TitleBar.png?fit=360%2C43&amp;ssl=1\" loading=\"lazy\" width=\"360\" height=\"43\" src=\"https:\/\/i2.wp.com\/engy.us\/blog\/wp-content\/uploads\/2020\/01\/Win10TitleBar.png?resize=360%2C43&#038;ssl=1\" alt=\"\" class=\"wp-image-121\" srcset=\"https:\/\/i2.wp.com\/engy.us\/blog\/wp-content\/uploads\/2020\/01\/Win10TitleBar.png?w=360&amp;ssl=1 360w, https:\/\/i2.wp.com\/engy.us\/blog\/wp-content\/uploads\/2020\/01\/Win10TitleBar.png?resize=300%2C36&amp;ssl=1 300w\" sizes=\"(max-width: 360px) 100vw, 360px\" data-recalc-dims=\"1\" \/><figcaption>Windows 10 standard title bar UI<\/figcaption><\/figure>\n\n\n\n<p>This will keep my UI consistent with the rest of Windows. I am choosing not to attempt to match the style for old versions of Windows, as I don&#8217;t think that would be a great time investment. It&#8217;s one little thing you lose when going with a full custom title bar, but it should be worth it by allowing full cohesive theming.  Buttons are 46 wide, 32 tall. <\/p>\n\n\n\n<h2>Building the UI<\/h2>\n\n\n\n<p>First, set WindowStyle=&#8221;None&#8221; on your Window. That will remove the built-in title bar and allow you to do everything on your own.<\/p>\n\n\n\n<p>Next, allocate space for your title bar in your UI. I like to allocate a Grid row for it.<\/p>\n\n\n\n<p>Add this under the &lt;Window&gt; node in your XAML:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;WindowChrome.WindowChrome&gt;\n    &lt;WindowChrome CaptionHeight=\"32\" ResizeBorderThickness=\"{x:Static SystemParameters.WindowResizeBorderThickness}\" \/&gt;\n&lt;\/WindowChrome.WindowChrome&gt;<\/code><\/pre>\n\n\n\n<p>The CaptionHeight tells the OS to treat the top 32px of your window as if it was a title bar. This means that click to drag works, along with double clicking to maximize\/restore, shaking to minimize other windows, etc. The ResizeBorderThickness allows the standard window resize logic to work, so we don&#8217;t need to reimplement that either.<\/p>\n\n\n\n<p>Now we need to make the actual UI. This is mine:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;Grid&gt;\n\t&lt;Grid.ColumnDefinitions&gt;\n\t\t&lt;ColumnDefinition Width=\"Auto\" \/&gt;\n\t\t&lt;ColumnDefinition Width=\"*\" \/&gt;\n\t\t&lt;ColumnDefinition Width=\"Auto\" \/&gt;\n\t\t&lt;ColumnDefinition Width=\"Auto\" \/&gt;\n\t\t&lt;ColumnDefinition Width=\"Auto\" \/&gt;\n\t&lt;\/Grid.ColumnDefinitions&gt;\n\t&lt;Image\n\t\tGrid.Column=\"0\"\n\t\tWidth=\"22\"\n\t\tHeight=\"22\"\n\t\tMargin=\"4\"\n\t\tSource=\"\/Icons\/VidCoder32.png\" \/&gt;\n\t&lt;TextBlock\n\t\tGrid.Column=\"1\"\n\t\tMargin=\"4 0 0 0\"\n\t\tVerticalAlignment=\"Center\"\n\t\tFontSize=\"14\"\n\t\tText=\"{Binding WindowTitle}\"&gt;\n\t\t&lt;TextBlock.Style&gt;\n\t\t\t&lt;Style TargetType=\"TextBlock\"&gt;\n\t\t\t\t&lt;Style.Triggers&gt;\n\t\t\t\t\t&lt;DataTrigger Binding=\"{Binding IsActive, RelativeSource={RelativeSource AncestorType=Window}}\" Value=\"False\"&gt;\n\t\t\t\t\t\t&lt;Setter Property=\"Foreground\" Value=\"{DynamicResource WindowTitleBarInactiveText}\" \/&gt;\n\t\t\t\t\t&lt;\/DataTrigger&gt;\n\t\t\t\t&lt;\/Style.Triggers&gt;\n\t\t\t&lt;\/Style&gt;\n\t\t&lt;\/TextBlock.Style&gt;\n\t&lt;\/TextBlock&gt;\n\n\t&lt;Button\n\t\tGrid.Column=\"2\"\n\t\tClick=\"OnMinimizeButtonClick\"\n\t\tRenderOptions.EdgeMode=\"Aliased\"\n\t\tStyle=\"{StaticResource TitleBarButtonStyle}\"&gt;\n\t\t&lt;Path\n\t\t\tWidth=\"46\"\n\t\t\tHeight=\"32\"\n\t\t\tData=\"M 18,15 H 28\"\n\t\t\tStroke=\"{Binding Path=Foreground,\n\t\t\t\t\t\t\t RelativeSource={RelativeSource AncestorType={x:Type Button}}}\"\n\t\t\tStrokeThickness=\"1\" \/&gt;\n\t&lt;\/Button&gt;\n\t&lt;Button\n\t\tName=\"maximizeButton\"\n\t\tGrid.Column=\"3\"\n\t\tClick=\"OnMaximizeRestoreButtonClick\"\n\t\tStyle=\"{StaticResource TitleBarButtonStyle}\"&gt;\n\t\t&lt;Path\n\t\t\tWidth=\"46\"\n\t\t\tHeight=\"32\"\n\t\t\tData=\"M 18.5,10.5 H 27.5 V 19.5 H 18.5 Z\"\n\t\t\tStroke=\"{Binding Path=Foreground,\n\t\t\t\t\t\t\t RelativeSource={RelativeSource AncestorType={x:Type Button}}}\"\n\t\t\tStrokeThickness=\"1\" \/&gt;\n\t&lt;\/Button&gt;\n\t&lt;Button\n\t\tName=\"restoreButton\"\n\t\tGrid.Column=\"3\"\n\t\tClick=\"OnMaximizeRestoreButtonClick\"\n\t\tStyle=\"{StaticResource TitleBarButtonStyle}\"&gt;\n\t\t&lt;Path\n\t\t\tWidth=\"46\"\n\t\t\tHeight=\"32\"\n\t\t\tData=\"M 18.5,12.5 H 25.5 V 19.5 H 18.5 Z M 20.5,12.5 V 10.5 H 27.5 V 17.5 H 25.5\"\n\t\t\tStroke=\"{Binding Path=Foreground,\n\t\t\t\t\t\t\t RelativeSource={RelativeSource AncestorType={x:Type Button}}}\"\n\t\t\tStrokeThickness=\"1\" \/&gt;\n\t&lt;\/Button&gt;\n\t&lt;Button\n\t\tGrid.Column=\"4\"\n\t\tClick=\"OnCloseButtonClick\"\n\t\tStyle=\"{StaticResource TitleBarCloseButtonStyle}\"&gt;\n\t\t&lt;Path\n\t\t\tWidth=\"46\"\n\t\t\tHeight=\"32\"\n\t\t\tData=\"M 18,11 27,20 M 18,20 27,11\"\n\t\t\tStroke=\"{Binding Path=Foreground,\n\t\t\t\t\t\t\t RelativeSource={RelativeSource AncestorType={x:Type Button}}}\"\n\t\t\tStrokeThickness=\"1\" \/&gt;\n\t&lt;\/Button&gt;\n&lt;\/Grid&gt;<\/code><\/pre>\n\n\n\n<p>I&#8217;ve got the app icon. I chose not to implement the special drop-down menu that comes with the standard title bar since it&#8217;s not often used and other major apps like Visual Studio Code don&#8217;t bother with it. But it&#8217;s certainly something you could add.<\/p>\n\n\n\n<p>The title text has a trigger to change its color based on the &#8220;Active&#8221; state of the window. That allows the user to better tell if the window has focus or not.<\/p>\n\n\n\n<p>The actual buttons use TitleBarButtonStyle and TitleBarCloseButtonStyle:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;Style x:Key=\"TitleBarButtonStyle\" TargetType=\"Button\"&gt;\n\t&lt;Setter Property=\"Foreground\" Value=\"{DynamicResource WindowTextBrush}\" \/&gt;\n\t&lt;Setter Property=\"Padding\" Value=\"0\" \/&gt;\n\t&lt;Setter Property=\"WindowChrome.IsHitTestVisibleInChrome\" Value=\"True\" \/&gt;\n\t&lt;Setter Property=\"IsTabStop\" Value=\"False\" \/&gt;\n\t&lt;Setter Property=\"Template\"&gt;\n\t\t&lt;Setter.Value&gt;\n\t\t\t&lt;ControlTemplate TargetType=\"{x:Type Button}\"&gt;\n\t\t\t\t&lt;Border\n\t\t\t\t\tx:Name=\"border\"\n\t\t\t\t\tBackground=\"Transparent\"\n\t\t\t\t\tBorderThickness=\"0\"\n\t\t\t\t\tSnapsToDevicePixels=\"true\"&gt;\n\t\t\t\t\t&lt;ContentPresenter\n\t\t\t\t\t\tx:Name=\"contentPresenter\"\n\t\t\t\t\t\tMargin=\"0\"\n\t\t\t\t\t\tHorizontalAlignment=\"Center\"\n\t\t\t\t\t\tVerticalAlignment=\"Center\"\n\t\t\t\t\t\tFocusable=\"False\"\n\t\t\t\t\t\tRecognizesAccessKey=\"True\" \/&gt;\n\t\t\t\t&lt;\/Border&gt;\n\t\t\t\t&lt;ControlTemplate.Triggers&gt;\n\t\t\t\t\t&lt;Trigger Property=\"IsMouseOver\" Value=\"true\"&gt;\n\t\t\t\t\t\t&lt;Setter TargetName=\"border\" Property=\"Background\" Value=\"{DynamicResource MouseOverOverlayBackgroundBrush}\" \/&gt;\n\t\t\t\t\t&lt;\/Trigger&gt;\n\t\t\t\t\t&lt;Trigger Property=\"IsPressed\" Value=\"true\"&gt;\n\t\t\t\t\t\t&lt;Setter TargetName=\"border\" Property=\"Background\" Value=\"{DynamicResource PressedOverlayBackgroundBrush}\" \/&gt;\n\t\t\t\t\t&lt;\/Trigger&gt;\n\t\t\t\t&lt;\/ControlTemplate.Triggers&gt;\n\t\t\t&lt;\/ControlTemplate&gt;\n\t\t&lt;\/Setter.Value&gt;\n\t&lt;\/Setter&gt;\n&lt;\/Style&gt;\n\n&lt;Style x:Key=\"TitleBarCloseButtonStyle\" TargetType=\"Button\"&gt;\n\t&lt;Setter Property=\"Foreground\" Value=\"{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}\" \/&gt;\n\t&lt;Setter Property=\"Padding\" Value=\"0\" \/&gt;\n\t&lt;Setter Property=\"WindowChrome.IsHitTestVisibleInChrome\" Value=\"True\" \/&gt;\n\t&lt;Setter Property=\"IsTabStop\" Value=\"False\" \/&gt;\n\t&lt;Setter Property=\"Template\"&gt;\n\t\t&lt;Setter.Value&gt;\n\t\t\t&lt;ControlTemplate TargetType=\"{x:Type Button}\"&gt;\n\t\t\t\t&lt;Border\n\t\t\t\t\tx:Name=\"border\"\n\t\t\t\t\tBackground=\"Transparent\"\n\t\t\t\t\tBorderThickness=\"0\"\n\t\t\t\t\tSnapsToDevicePixels=\"true\"&gt;\n\t\t\t\t\t&lt;ContentPresenter\n\t\t\t\t\t\tx:Name=\"contentPresenter\"\n\t\t\t\t\t\tMargin=\"0\"\n\t\t\t\t\t\tHorizontalAlignment=\"Center\"\n\t\t\t\t\t\tVerticalAlignment=\"Center\"\n\t\t\t\t\t\tFocusable=\"False\"\n\t\t\t\t\t\tRecognizesAccessKey=\"True\" \/&gt;\n\t\t\t\t&lt;\/Border&gt;\n\t\t\t\t&lt;ControlTemplate.Triggers&gt;\n\t\t\t\t\t&lt;Trigger Property=\"IsMouseOver\" Value=\"true\"&gt;\n\t\t\t\t\t\t&lt;Setter TargetName=\"border\" Property=\"Background\" Value=\"{DynamicResource MouseOverWindowCloseButtonBackgroundBrush}\" \/&gt;\n\t\t\t\t\t\t&lt;Setter Property=\"Foreground\" Value=\"{DynamicResource MouseOverWindowCloseButtonForegroundBrush}\" \/&gt;\n\t\t\t\t\t&lt;\/Trigger&gt;\n\t\t\t\t\t&lt;Trigger Property=\"IsPressed\" Value=\"true\"&gt;\n\t\t\t\t\t\t&lt;Setter TargetName=\"border\" Property=\"Background\" Value=\"{DynamicResource PressedWindowCloseButtonBackgroundBrush}\" \/&gt;\n\t\t\t\t\t\t&lt;Setter Property=\"Foreground\" Value=\"{DynamicResource MouseOverWindowCloseButtonForegroundBrush}\" \/&gt;\n\t\t\t\t\t&lt;\/Trigger&gt;\n\t\t\t\t&lt;\/ControlTemplate.Triggers&gt;\n\t\t\t&lt;\/ControlTemplate&gt;\n\t\t&lt;\/Setter.Value&gt;\n\t&lt;\/Setter&gt;\n&lt;\/Style&gt;<\/code><\/pre>\n\n\n\n<p>These are buttons with stripped-down control templates to remove a lot of the extra gunk. They have triggers to change the background color on mouse over (and the foreground color in the case of the Close button). Also they set WindowChrome.IsHitTestVisibleInChrome to True, which allows them to pick up clicks even though they are in the 32px caption area we set up earlier.<\/p>\n\n\n\n<p>The button content itself uses &lt;Path&gt; to draw the icons. The minimize button uses RenderOptions.EdgeMode=&#8221;Aliased&#8221; to disable anti-aliasing and make sure it renders crisply without blurring over into other pixels. I set the Stroke to pick up the Foreground color from the parent button. The Path data for the maximize\/restore buttons are all set on .5 to make sure it renders cleanly at the standard 96 DPI. With whole numbers it ends up drawing on the edge of the pixel and blurring the lines. We can&#8217;t use the same &#8220;Aliased&#8221; trick here as that might cause the pixel count for different lines to change and look off at different zoom levels like 125%\/150%.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img data-attachment-id=\"122\" data-permalink=\"https:\/\/engy.us\/blog\/2020\/01\/01\/implementing-a-custom-window-title-bar-in-wpf\/darktitlebar\/\" data-orig-file=\"https:\/\/i1.wp.com\/engy.us\/blog\/wp-content\/uploads\/2020\/01\/DarkTitleBar.png?fit=622%2C58&amp;ssl=1\" data-orig-size=\"622,58\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;}\" data-image-title=\"DarkTitleBar\" data-image-description=\"\" data-medium-file=\"https:\/\/i1.wp.com\/engy.us\/blog\/wp-content\/uploads\/2020\/01\/DarkTitleBar.png?fit=300%2C28&amp;ssl=1\" data-large-file=\"https:\/\/i1.wp.com\/engy.us\/blog\/wp-content\/uploads\/2020\/01\/DarkTitleBar.png?fit=525%2C49&amp;ssl=1\" loading=\"lazy\" width=\"525\" height=\"49\" src=\"https:\/\/i1.wp.com\/engy.us\/blog\/wp-content\/uploads\/2020\/01\/DarkTitleBar.png?resize=525%2C49&#038;ssl=1\" alt=\"\" class=\"wp-image-122\" srcset=\"https:\/\/i1.wp.com\/engy.us\/blog\/wp-content\/uploads\/2020\/01\/DarkTitleBar.png?w=622&amp;ssl=1 622w, https:\/\/i1.wp.com\/engy.us\/blog\/wp-content\/uploads\/2020\/01\/DarkTitleBar.png?resize=300%2C28&amp;ssl=1 300w\" sizes=\"(max-width: 525px) 100vw, 525px\" data-recalc-dims=\"1\" \/><figcaption>Looking good!<\/figcaption><\/figure>\n\n\n\n<h2>Responding to button clicks<\/h2>\n\n\n\n<p>Now that we have the UI in place, we need to respond to those button clicks. I normally use databinding\/MVVM, but in this case I decided to bypass the viewmodel since these are really talking directly to the window.<\/p>\n\n\n\n<p>Event handler methods:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>private void OnMinimizeButtonClick(object sender, RoutedEventArgs e)\n{\n\tthis.WindowState = WindowState.Minimized;\n}\n\nprivate void OnMaximizeRestoreButtonClick(object sender, RoutedEventArgs e)\n{\n\tif (this.WindowState == WindowState.Maximized)\n\t{\n\t\tthis.WindowState = WindowState.Normal;\n\t}\n\telse\n\t{\n\t\tthis.WindowState = WindowState.Maximized;\n\t}\n}\n\nprivate void OnCloseButtonClick(object sender, RoutedEventArgs e)\n{\n\tthis.Close();\n}<\/code><\/pre>\n\n\n\n<p>Helper method to refresh the maximize\/restore button:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>private void RefreshMaximizeRestoreButton()\n{\n\tif (this.WindowState == WindowState.Maximized)\n\t{\n\t\tthis.maximizeButton.Visibility = Visibility.Collapsed;\n\t\tthis.restoreButton.Visibility = Visibility.Visible;\n\t}\n\telse\n\t{\n\t\tthis.maximizeButton.Visibility = Visibility.Visible;\n\t\tthis.restoreButton.Visibility = Visibility.Collapsed;\n\t}\n}<\/code><\/pre>\n\n\n\n<p>This, we call in the constructor and in an event handler for Window.StateChanged:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>private void Window_StateChanged(object sender, EventArgs e)\n{\n\tthis.RefreshMaximizeRestoreButton();\n}<\/code><\/pre>\n\n\n\n<p>This will make sure the button displays correctly no matter how the window state change is invoked.<\/p>\n\n\n\n<h2>Maximized placement<\/h2>\n\n\n\n<p>You thought we were done? Hah. Windows has other plans. You might notice that when you maximize your window, some content is getting cut off and it&#8217;s hiding your task bar. The default values it picks for maximized window placement are really weird, where it cuts off 7px of your window content and doesn&#8217;t account for task bar placement.<\/p>\n\n\n\n<p>To fix this, we need to listen for the <a href=\"https:\/\/docs.microsoft.com\/en-us\/windows\/win32\/winmsg\/wm-getminmaxinfo\">WM_GETMINMAXINFO WndProc message<\/a> to tell our window it needs to go somewhere different when maximize. Put this in your window codebehind:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>protected override void OnSourceInitialized(EventArgs e)\n{\n\tbase.OnSourceInitialized(e);\n\t((HwndSource)PresentationSource.FromVisual(this)).AddHook(HookProc);\n}\n\npublic static IntPtr HookProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)\n{\n\tif (msg == WM_GETMINMAXINFO)\n\t{\n\t\t\/\/ We need to tell the system what our size should be when maximized. Otherwise it will cover the whole screen,\n\t\t\/\/ including the task bar.\n\t\tMINMAXINFO mmi = (MINMAXINFO)Marshal.PtrToStructure(lParam, typeof(MINMAXINFO));\n\n\t\t\/\/ Adjust the maximized size and position to fit the work area of the correct monitor\n\t\tIntPtr monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);\n\n\t\tif (monitor != IntPtr.Zero)\n\t\t{\n\t\t\tMONITORINFO monitorInfo = new MONITORINFO();\n\t\t\tmonitorInfo.cbSize = Marshal.SizeOf(typeof(MONITORINFO));\n\t\t\tGetMonitorInfo(monitor, ref monitorInfo);\n\t\t\tRECT rcWorkArea = monitorInfo.rcWork;\n\t\t\tRECT rcMonitorArea = monitorInfo.rcMonitor;\n\t\t\tmmi.ptMaxPosition.X = Math.Abs(rcWorkArea.Left - rcMonitorArea.Left);\n\t\t\tmmi.ptMaxPosition.Y = Math.Abs(rcWorkArea.Top - rcMonitorArea.Top);\n\t\t\tmmi.ptMaxSize.X = Math.Abs(rcWorkArea.Right - rcWorkArea.Left);\n\t\t\tmmi.ptMaxSize.Y = Math.Abs(rcWorkArea.Bottom - rcWorkArea.Top);\n\t\t}\n\n\t\tMarshal.StructureToPtr(mmi, lParam, true);\n\t}\n\n\treturn IntPtr.Zero;\n}\n\nprivate const int WM_GETMINMAXINFO = 0x0024;\n\nprivate const uint MONITOR_DEFAULTTONEAREST = 0x00000002;\n\n&#91;DllImport(\"user32.dll\")]\nprivate static extern IntPtr MonitorFromWindow(IntPtr handle, uint flags);\n\n&#91;DllImport(\"user32.dll\")]\nprivate static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO lpmi);\n\n&#91;Serializable]\n&#91;StructLayout(LayoutKind.Sequential)]\npublic struct RECT\n{\n\tpublic int Left;\n\tpublic int Top;\n\tpublic int Right;\n\tpublic int Bottom;\n\n\tpublic RECT(int left, int top, int right, int bottom)\n\t{\n\t\tthis.Left = left;\n\t\tthis.Top = top;\n\t\tthis.Right = right;\n\t\tthis.Bottom = bottom;\n\t}\n}\n\n&#91;StructLayout(LayoutKind.Sequential)]\npublic struct MONITORINFO\n{\n\tpublic int cbSize;\n\tpublic RECT rcMonitor;\n\tpublic RECT rcWork;\n\tpublic uint dwFlags;\n}\n\n&#91;Serializable]\n&#91;StructLayout(LayoutKind.Sequential)]\npublic struct POINT\n{\n\tpublic int X;\n\tpublic int Y;\n\n\tpublic POINT(int x, int y)\n\t{\n\t\tthis.X = x;\n\t\tthis.Y = y;\n\t}\n}\n\n&#91;StructLayout(LayoutKind.Sequential)]\npublic struct MINMAXINFO\n{\n\tpublic POINT ptReserved;\n\tpublic POINT ptMaxSize;\n\tpublic POINT ptMaxPosition;\n\tpublic POINT ptMinTrackSize;\n\tpublic POINT ptMaxTrackSize;\n}<\/code><\/pre>\n\n\n\n<p>When the system asks the window where it should be when it maximizes, this code will ask what monitor it&#8217;s on, then place itself in the work area of the monitor (not overlapping the task bar).<\/p>\n\n\n\n<h2>Declare ourselves DPI-aware<\/h2>\n\n\n\n<p>In order to get the WM_GETMINMAXINFO handler to behave correctly, we need to declare our app as per-monitor DPI aware to the unmanaged system.<\/p>\n\n\n\n<p>Right click on your project and select <strong>Add ->  New Item -> Application Manifest File (Windows Only)<\/strong> .<\/p>\n\n\n\n<p>Add these settings to declare us as properly DPI-aware:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>  &lt;application xmlns=\"urn:schemas-microsoft-com:asm.v3\">\r\n    &lt;windowsSettings>\r\n      &lt;dpiAwareness xmlns=\"http:\/\/schemas.microsoft.com\/SMI\/2016\/WindowsSettings\">PerMonitor&lt;\/dpiAwareness>\r\n      &lt;dpiAware xmlns=\"http:\/\/schemas.microsoft.com\/SMI\/2005\/WindowsSettings\">true&lt;\/dpiAware>\r\n    &lt;\/windowsSettings>\r\n  &lt;\/application><\/code><\/pre>\n\n\n\n<p>Otherwise with multi-monitor setups you can get maximized windows that stretch off the screen. This setting is a little confusing because WPF is by default DPI aware and normally works just fine out of the box on multiple monitors. But this is required to let the unmanaged APIs that we are hooking into know how WPF is handling things.<\/p>\n\n\n\n<h2>Window border<\/h2>\n\n\n\n<p>Finally, the window can be kind of hard to pick out when it doesn&#8217;t have a border, if it&#8217;s put against the wrong background. Let&#8217;s fix that now. Wrap your window UI in this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;Border Style=\"{StaticResource WindowMainPanelStyle}\"&gt;\n    ...your UI\n&lt;\/Border&gt;<\/code><\/pre>\n\n\n\n<p>This is the style:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;Style x:Key=\"WindowMainPanelStyle\" TargetType=\"{x:Type Border}\"&gt;\n    &lt;Setter Property=\"BorderBrush\" Value=\"{DynamicResource WindowBorderBrush}\" \/&gt;\n    &lt;Setter Property=\"BorderThickness\" Value=\"1\" \/&gt;\n    &lt;Style.Triggers&gt;\n        &lt;DataTrigger Binding=\"{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=WindowState}\" Value=\"Maximized\"&gt;\n            &lt;Setter Property=\"BorderThickness\" Value=\"0\" \/&gt;\n        &lt;\/DataTrigger&gt;\n    &lt;\/Style.Triggers&gt;\n&lt;\/Style&gt;<\/code><\/pre>\n\n\n\n<p>This will remove the 1px border when the window is maximized.<\/p>\n\n\n\n<h2>Okay, now we&#8217;re actually done<\/h2>\n\n\n\n<p>At least until Windows decides they want to shake up the title bar style again.<br><\/p>\n","protected":false},"excerpt":{"rendered":"<p>There are several good reasons for wanting custom window chrome in WPF, such as fitting in additional UI or implementing a Dark theme. However the actual implementation is kind of tricky, since it is now your job to provide a bunch of features that you used to get for free. I&#8217;ll walk you through my &hellip; <\/p>\n<p class=\"link-more\"><a href=\"https:\/\/engy.us\/blog\/2020\/01\/01\/implementing-a-custom-window-title-bar-in-wpf\/\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;Implementing a Custom Window Title Bar in WPF&#8221;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"spay_email":"","jetpack_publicize_message":"","jetpack_is_tweetstorm":false},"categories":[1],"tags":[],"jetpack_featured_media_url":"","jetpack_publicize_connections":[],"jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/pahBcK-1W","jetpack-related-posts":[{"id":88,"url":"https:\/\/engy.us\/blog\/2018\/10\/20\/dark-theme-in-wpf\/","url_meta":{"origin":120,"position":0},"title":"Dark Theme in WPF","date":"October 20, 2018","format":false,"excerpt":"In a recent Windows 10 update a toggle switch was added to allow the user to specify that they wanted \"Dark\" themes in apps: I decided to add support for this to VidCoder. But no updates to WPF were made to make this easy. WPF does having theming support where\u2026","rel":"","context":"With 5 comments","img":{"alt_text":"","src":"https:\/\/i2.wp.com\/engy.us\/blog\/wp-content\/uploads\/2018\/10\/VidCoderDarkExample.png?resize=350%2C200&ssl=1","width":350,"height":200},"classes":[]},{"id":79,"url":"https:\/\/engy.us\/blog\/2018\/10\/03\/wpf-databinding-using-dynamicdata\/","url_meta":{"origin":120,"position":1},"title":"WPF Databinding using DynamicData","date":"October 3, 2018","format":false,"excerpt":"My project is MVVM and I had been using ReactiveList<T> as my go-to observable collection for viewmodel properties. But ReactiveUI deprecated ReactiveList in 8.6.1. So I needed to get on to the new recommended library: DynamicData. But there is no direct drop-in replacement you can do like ObservableCollection<T> <-> ReactiveList<T>.\u2026","rel":"","context":"With 2 comments","img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":43,"url":"https:\/\/engy.us\/blog\/2010\/03\/08\/saving-window-size-and-location-in-wpf-and-winforms\/","url_meta":{"origin":120,"position":2},"title":"Saving window size and location in WPF and WinForms","date":"March 8, 2010","format":false,"excerpt":"A common user interface tweak is to save the size and location of a window and restore that state when the program starts up again. That way, the users don\u2019t need to re-do their work of resizing and moving the windows to where they want them. However I\u2019ve found that\u2026","rel":"","context":"With 21 comments","img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":33,"url":"https:\/\/engy.us\/blog\/2010\/03\/31\/using-the-dispatcher-with-mvvm\/","url_meta":{"origin":120,"position":3},"title":"Using the Dispatcher with MVVM","date":"March 31, 2010","format":false,"excerpt":"When writing an MVVM application, you want to separate from the UI. However you also need to make sure that UI updates happen on the UI thread. Changes made through INotifyPropertyChanged get automatically marshaled to the UI thread, so in most cases you\u2019ll be fine. However, when using INotifyCollectionChanged (such\u2026","rel":"","context":"With 12 comments","img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":51,"url":"https:\/\/engy.us\/blog\/2015\/07\/17\/installing-net-framework-4-5-automatically-with-inno-setup\/","url_meta":{"origin":120,"position":4},"title":"Installing .NET Framework 4.7 automatically with Inno Setup","date":"July 17, 2015","format":false,"excerpt":"In this guide I will walk through how to get the .NET framework to download and install on-the-fly in an Inno Setup installer. It works in 3 steps: Detect if the desired .NET framework is installed Download the .NET Framework bootstrap installer with Inno Download Plugin Run the bootstrap installer\u2026","rel":"","context":"In \".net framework 4.5 inno setup\"","img":{"alt_text":"","src":"https:\/\/i0.wp.com\/engy.us\/blog\/wp-content\/uploads\/2015\/07\/7028.InstallingFramework2-1.png?resize=350%2C200&ssl=1","width":350,"height":200},"classes":[]},{"id":146,"url":"https:\/\/engy.us\/blog\/2021\/02\/28\/installing-net-5-runtime-automatically-with-inno-setup\/","url_meta":{"origin":120,"position":5},"title":"Installing .NET 5 Runtime Automatically with Inno Setup","date":"February 28, 2021","format":false,"excerpt":"In this guide I will walk through how to get the .NET 5 runtime to download and install on-the-fly in an Inno Setup installer. It works in 3 steps: Detect if the desired .NET runtime is installedDownload the .NET Runtime bootstrap installer with Inno Download PluginRun the bootstrap installer in\u2026","rel":"","context":"With 2 comments","img":{"alt_text":"","src":"https:\/\/i0.wp.com\/engy.us\/blog\/wp-content\/uploads\/2021\/02\/image.png?resize=350%2C200&ssl=1","width":350,"height":200},"classes":[]}],"_links":{"self":[{"href":"https:\/\/engy.us\/blog\/wp-json\/wp\/v2\/posts\/120"}],"collection":[{"href":"https:\/\/engy.us\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/engy.us\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/engy.us\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/engy.us\/blog\/wp-json\/wp\/v2\/comments?post=120"}],"version-history":[{"count":6,"href":"https:\/\/engy.us\/blog\/wp-json\/wp\/v2\/posts\/120\/revisions"}],"predecessor-version":[{"id":172,"href":"https:\/\/engy.us\/blog\/wp-json\/wp\/v2\/posts\/120\/revisions\/172"}],"wp:attachment":[{"href":"https:\/\/engy.us\/blog\/wp-json\/wp\/v2\/media?parent=120"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/engy.us\/blog\/wp-json\/wp\/v2\/categories?post=120"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/engy.us\/blog\/wp-json\/wp\/v2\/tags?post=120"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}