47

I'm using the WPF Shell Integration Library to create a custom chrome of my wpf app. All is good, but when maximizing the app, 6 or 7 pixels are out of the screen.

This is the code I'm using:

<Style TargetType="{x:Type local:MainWindow}">
        <Setter Property="shell:WindowChrome.WindowChrome">
            <Setter.Value>
                <shell:WindowChrome
                    ResizeBorderThickness="6"
                    CaptionHeight="10"
                    CornerRadius="0"
                    GlassFrameThickness="1"/>
            </Setter.Value>
        </Setter>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:MainWindow}">
                    <Grid>
                        <Border BorderThickness="1" BorderBrush="#389FD1" Background="#389FD1">
                            <ContentPresenter Margin="0,22,0,0" Content="{TemplateBinding Content}"/>
                        </Border>
                        <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Top" >
                            <TextBlock Text="{Binding NombreUsuario}" Foreground="White" Margin="5,5,20,5" Opacity=".8" />
                            <Button Style="{StaticResource ImageButton}" Height="20" Width="20" Margin="0" Click="WindowMinimize" shell:WindowChrome.IsHitTestVisibleInChrome="True">
                                <Image Height="10" Width="10" Source="/Resources/Images/minimize.png" />
                            </Button>
                            <Button Style="{StaticResource ImageButton}" Height="20" Width="20" Margin="0" Click="WindowMaximizeRestore" shell:WindowChrome.IsHitTestVisibleInChrome="True" >
                                <Image Height="10" Width="10" Source="/Resources/Images/maximize.png" />
                            </Button>
                            <Button Style="{StaticResource ImageButton}" Height="20" Width="20" Margin="0" Click="WindowClose" shell:WindowChrome.IsHitTestVisibleInChrome="True">
                                <Image Height="10" Width="10" Source="/Resources/Images/close.png" />
                            </Button>
                        </StackPanel>

                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
Ilmari Karonen
  • 49,047
  • 9
  • 93
  • 153
Eduardo Molteni
  • 38,786
  • 23
  • 141
  • 206
  • This also occurs when using the `System.Windows.Shell.WindowChrome` class (the NuGet package referred to here is no longer needed as this class is now built in to `PresentationFramework.dll`). – Drew Noakes Oct 11 '16 at 14:22
  • This is useful: https://stackoverflow.com/questions/4670929/checking-the-value-of-the-windows-windowstate-in-a-trigger – Contango Feb 19 '19 at 09:51

7 Answers7

53

Windows crops the edges of the window when it's maximized to obscure what would normally be the resize edges. You can get around this by putting a proxy border between the window and your content and then inflate the thickness when it's maximized.

I modified the example that came with the lib to do this, the same basic change could be made to your sample:

<ControlTemplate TargetType="{x:Type local:SelectableChromeWindow}">
  <Border BorderBrush="Green">
    <Border.Style>
      <Style TargetType="{x:Type Border}">
        <Setter Property="BorderThickness" Value="0"/>
        <Style.Triggers>
          <DataTrigger Binding="{Binding ElementName=ThisWindow, Path=WindowState}" Value="Maximized">
            <Setter Property="BorderThickness" Value="{Binding Source={x:Static shell:SystemParameters2.Current}, Path=WindowResizeBorderThickness}"/>
          </DataTrigger>
        </Style.Triggers>
      </Style>
    </Border.Style>
    <Grid [...]/>
  </Border>
</ControlTemplate>

I hope that helps.

For .net 4.5 and above, the SystemParameters are a little different, e.g.:

<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:MainWindow}}, Path=WindowState}" Value="Maximized">
    <Setter Property="BorderThickness" Value="{Binding Source={x:Static SystemParameters.WindowResizeBorderThickness}}"/>
</DataTrigger>
Vimes
  • 10,577
  • 17
  • 66
  • 86
Joe Castro
  • 2,181
  • 18
  • 24
8

this worked for me:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                xmlns:shell="clr-namespace:Microsoft.Windows.Shell;assembly=Microsoft.Windows.Shell"
                xmlns:Views="clr-namespace:BorderLessWpf.Views">

<Style TargetType="{x:Type Views:ShellView}" >
    <Setter Property="shell:WindowChrome.WindowChrome">
        <Setter.Value>
            <shell:WindowChrome
                ResizeBorderThickness="6"
                CaptionHeight="10"
                CornerRadius="0"
                GlassFrameThickness="1"/>
        </Setter.Value>
    </Setter>
    <Style.Triggers>
        <Trigger Property="WindowState" Value="Maximized">
            <Setter Property="BorderThickness" Value="6" />
        </Trigger>
    </Style.Triggers>
</Style>

Bas
  • 678
  • 6
  • 10
7

Why is this happening?

Raymond Chen has explained this in his note Why are the dimensions of a maximized window larger than the monitor?:

The extra eight pixels that hang off the screen are the window borders. When you maximize a window, the window manager arranges things so that the client area of the window fills the width of your work area, and it plus the caption bar fills the height of the work area. After all, you want to see as much of your document as possible; there’s no need to show you the window borders that aren’t doing you any good. (It leaves the caption bar on the screen for obvious reasons.)

How to handle it?

@Joe Castro has proposed the right approach. You basically have to add a border (or specify a margin) around your window each time it maximizes. And SystemParameters.WindowResizeBorderThickness is also the right property, but ... it has a bug.

For me, it gives 4px border instead of correct 8px for 100% display scale (can be changed in Display Settings) and approximately 3px instead of correct 7px for 175%.

Why I'm so sure it's a bug and not just the wrong property?

Internally SystemParameters.WindowResizeBorderThickness uses GetSystemMetrics call to obtain device pixels of the border. It queries for CXFRAME (32) and CYFRAME(33) indexes, and then converts it into logical pixels.

Now check this old bug report:

Take the following lines of code:

    int cx = ::GetSystemMetrics(SM_CXSIZEFRAME);
    int cy = ::GetSystemMetrics(SM_CYSIZEFRAME);
    int cp = ::GetSystemMetrics(SM_CYCAPTION);

When compiled with Visual Studio 2010, on my Windows7 machine this yields: cx == 9, cy == 9 and cp == 27.

If I compile the very same code with Vision Studio 2012 RC on the same machine, this yields: cx == 5, cy == 5 and cp == 27.

And the answer:

Posted by Microsoft on 7/12/2012 at 11:03 AM Thank you for your bug submission. The issue you reported appears to be a Windows issue, and we have forwarded the bug to them.

But there's some good news from this question: GetSystemMetrics() returns different results for .NET 4.5 & .NET 4.0

For .NET 4.5 and later you can just add SM_CXPADDEDBORDER (92) value to both CXFRAME (32) and CYFRAME(33) to get the right values.

What's the final solution?

Use @Joe Castro approach, but with the fixed property:

public static class SystemParametersFix
{
    public static Thickness WindowResizeBorderThickness
    {
        get
        {
            float dpix = GetDpi(GetDeviceCapsIndex.LOGPIXELSX);
            float dpiy = GetDpi(GetDeviceCapsIndex.LOGPIXELSY);

            int dx = GetSystemMetrics(GetSystemMetricsIndex.CXFRAME);
            int dy = GetSystemMetrics(GetSystemMetricsIndex.CYFRAME);

            // this adjustment is needed only since .NET 4.5 
            int d = GetSystemMetrics(GetSystemMetricsIndex.SM_CXPADDEDBORDER);
            dx += d;
            dy += d;

            var leftBorder = dx / dpix;
            var topBorder = dy / dpiy;

            return new Thickness(leftBorder, topBorder, leftBorder, topBorder);
        }
    }

    [DllImport("user32.dll")]
    private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);

    [DllImport("user32.dll")]
    private static extern IntPtr GetDC(IntPtr hwnd);

    [DllImport("gdi32.dll")]
    private static extern int GetDeviceCaps(IntPtr hdc, int nIndex);

    private static float GetDpi(GetDeviceCapsIndex index)
    {
        IntPtr desktopWnd = IntPtr.Zero;
        IntPtr dc = GetDC(desktopWnd);
        float dpi;
        try
        {
            dpi = GetDeviceCaps(dc, (int)index);
        }
        finally
        {
            ReleaseDC(desktopWnd, dc);
        }
        return dpi / 96f;
    }

    private enum GetDeviceCapsIndex
    {
        LOGPIXELSX = 88,
        LOGPIXELSY = 90
    }

    [DllImport("user32.dll")]
    private static extern int GetSystemMetrics(GetSystemMetricsIndex nIndex);

    private enum GetSystemMetricsIndex
    {
        CXFRAME = 32,
        CYFRAME = 33,
        SM_CXPADDEDBORDER = 92
    }
}
astef
  • 8,575
  • 4
  • 56
  • 95
  • 1
    For what it's worth, I don't think this is quite a .Net4 vs .Net4.5 bug. The bug that you linked to matches what I expected, where this is related to linking to Comctl32 v6 vs v5. That's why the 2012 RC version had this "bug" as well. In my original version of SystemParameters2 I didn't add in the SM_CXPADDEDBORDER but I didn't test with Comctl 6. I remember this coming up in the VS2010 timeframe but it was an unacceptable breaking change to make 6v default. Looks like in newer versions they went the other way. – Joe Castro Apr 21 '20 at 01:08
  • 1
    @astef, can you mention, how (where) to use the fixed property? I'm trying to fix the same issue in a `RibbonWindow` – dan Aug 01 '20 at 09:07
  • Posting the xaml so show how the final product should look would be really helpful. – On The Net Again Jul 30 '21 at 13:49
3

In my project, I have CaptionHeight set to 0 and ResizeMode set to CanResizeWithGrip. This is the code I came up with for the proper thickness.

            Thickness maximizeFix = new Thickness(SystemParameters.WindowNonClientFrameThickness.Left +
                SystemParameters.WindowResizeBorderThickness.Left,
                SystemParameters.WindowNonClientFrameThickness.Top +
                SystemParameters.WindowResizeBorderThickness.Top
                - SystemParameters.CaptionHeight,
                SystemParameters.WindowNonClientFrameThickness.Right +
                SystemParameters.WindowResizeBorderThickness.Right,
                SystemParameters.WindowNonClientFrameThickness.Bottom +
                SystemParameters.WindowResizeBorderThickness.Bottom);

I'm just going to use a border like Joe Castro did and then bind the thickness to a property that I update when the window state changes.

Code seems janky but I haven't found another solution yet.

  • If you reference another answer from your answer, consider linking to it, or incorporating part of it (with attribution) to make your answer complete. – Jeffrey Bosboom Feb 21 '16 at 05:19
  • For some reason on Windows 10 this code gives incorrect thikness. It should be 6.5 on my PC but code calculates 5. – Andrew Mikhailov Mar 16 '18 at 07:20
  • Adding anything else except `WindowResizeBorderThickness` is a mistake. Read my answer to understand why `WindowResizeBorderThickness` is the only right property, and how to make it work – astef Apr 19 '20 at 03:46
1

From here, I thought I would try setting WindowChrome.NonClientFrameEdges.

With some experimentation it turns out that you can get the desired effect by setting it to NonClientFrameEdges.Bottom when maximised, and setting it back to NonClientFrameEdges.None when not maximised.

This makes the margin disappear on all edges except the bottom, but crucially causes the overlap under the taskbar to no longer happen.

It is important that the WindowChrome is set prior to the WindowState, so in my code-behind I have the functions (MainGrid2 holds all the window content barring the status bar, and StatusGrid holds the status bar):

    private void Maximise()
    {
        StatusGrid.Margin = new Thickness(7, 0, 7, 0);
        MainGrid2.Margin = new Thickness(12, 12, 12, 0);            
        chrome.NonClientFrameEdges = NonClientFrameEdges.Bottom;
        WindowChrome.SetWindowChrome(this, chrome); 
        WindowState = System.Windows.WindowState.Maximized;
    }

    private void Restore()
    {
        StatusGrid.Margin = new Thickness(0);
        MainGrid2.Margin = new Thickness(5, 5, 5, 0);
        chrome.NonClientFrameEdges = NonClientFrameEdges.None;
        WindowChrome.SetWindowChrome(this, chrome);
        WindowState = System.Windows.WindowState.Normal;
    }
Ethaan
  • 11,291
  • 5
  • 35
  • 45
Ben Sewell
  • 11
  • 2
1

To solve the issue of the window getting out of the screen simply set:

this.ResizeMode = System.Windows.ResizeMode.NoResize;

See JAPF's "Set up the main window of your WPF project" post for more info.

Nathan Tuggy
  • 2,237
  • 27
  • 30
  • 38
Ayo
  • 45
  • 1
0

I managed to solve the problem by adding a border around the main content of the window and bound to a property in the ViewModel. When the user presses the maximize button a command is triggered that sets the BorderThickness to 7. When the maximize button is pressed again the BorderThickness is set to 0.

    <!-- Content of my window -->
    <Border BorderThickness="{Binding BorderThickness}" BorderBrush="{StaticResource OffsetBlackBrush}">
            <Grid>
<StackPanel Grid.Column="2" Orientation="Horizontal">
                    <Button Content="-" Command="{Binding MinimizeCommand}" Style="{StaticResource WindowControlButton}" />
                    <Button Content="[ ]" Command="{Binding MaximizeCommand}" Style="{StaticResource WindowControlButton}" />
                    <Button Content="X" Command="{Binding CloseCommand}" Style="{StaticResource WindowCloseButton}" />
                </StackPanel>
            </Grid>
    </Border>


<!-- BorderThickness property in VM -->
public int BorderThickness { get; set; } = 0;


    /// <summary>
    /// Maximizes the current window
    /// </summary>
    public RelayCommand MaximizeCommand
    {
        get => new RelayCommand(param =>
        {
            if (!_view.GetWindowState())
            {
                BorderThickness = 7;
                _view.SetWindowState("Maximize"); // Change window state to maximized using interface passed into ctor
            }
            else
            {
                BorderThickness = 0;
                _view.SetWindowState("Normal");
            }
        });
    }
Craig Martin
  • 179
  • 2
  • 15
  • This will leave 1px line for 100% display scale. And it will still cut your window for large scales (more than 200%). See my answer to understand how to remove this magic number. – astef Apr 19 '20 at 03:52