using System; using System.ComponentModel; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Controls; using System.Windows.Forms; using System.Windows.Input; using System.Windows.Interop; namespace CustomControls { [DesignTimeVisible(false)] public partial class ChromeWindow : Window, IForm { private IntPtr handle; public System.Windows.Controls.ContentControl ContentControl => this.AppArea; private bool extendedAppArea; public bool ExtendedAppArea { get => extendedAppArea; set { extendedAppArea = value; Grid.SetRow(this.AppArea, value ? 0 : 1); Grid.SetRowSpan(this.AppArea, value ? 2 : 1); } } private double extendedAppAreaSize; public double ExtendedAppAreaSize { get => extendedAppAreaSize; set { extendedAppAreaSize = value; this.TitleArea.Margin = new Thickness(value, 0, 0, 0); this.TitleDock.Margin = new Thickness(-value, 0, 0, 0); this.TitleText.Margin = new Thickness(value + 8, 0, 140, 0); } } public System.Drawing.Size ClientSizeDelta { get { double scale = Helper.GetDpiScale(this); return new System.Drawing.Size(0, (int)(scale * (ExtendedAppArea ? 0 : TitleArea.Height))); } } private FormBorderStyle formBorderStyle; public FormBorderStyle FormBorderStyle { get => formBorderStyle; set { formBorderStyle = value; MinimizeBox = MaximizeBox = value == FormBorderStyle.Sizable || value == FormBorderStyle.SizableToolWindow; ShowIcon = value == FormBorderStyle.Sizable; } } private bool maximizeBox; public bool MaximizeBox { get => maximizeBox; set { maximizeBox = value; UpdateMaximizeRestoreState(); } } private bool minimizeBox; public bool MinimizeBox { get => minimizeBox; set { minimizeBox = value; MinimizeButton.Visibility = value ? Visibility.Visible : Visibility.Collapsed; } } public bool ShowIcon { get => TitleIcon.Visibility == Visibility.Visible; set => TitleIcon.Visibility = value ? Visibility.Visible : Visibility.Collapsed; } public ChromeWindow() { InitializeComponent(); StateChanged += (s, e) => UpdateMaximizeRestoreState(); SourceInitialized += (s, e) => { handle = new WindowInteropHelper(this).Handle; HwndSource.FromHwnd(handle).AddHook(WindowProc); UpdateMaximizeRestoreState(); }; } private IntPtr WindowProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { const int WM_GETMINMAXINFO = 0x24; const int WM_NCCALCSIZE = 0x83; const int WM_NCHITTEST = 0x84; const int HT_CLIENT = 1; // 1. in order to use snap features, we have to set the ResizeMode to CanResize // 2. setting WindowStyle to SingleBorderWindow is required to get maximize/restore animations // 3. use WM_NCCALCSIZE (handle and return 0) to remove non-client area // 4. the maximized window's client area is off now. Don't try to reduce it using wpf margins: on a multi-monitor system this will degrade rendering performance // 5. update WM_NCCALCSIZE to return smaller client area when maximized // 6. now we have popping effect when maximized window is shown first time (due to changed nc size) // 7. handle WM_GETMINMAXINFO to return smaller maximized bounds to cheat Windows // 8. the only side effect we have is a single pixel gap at the bottom of maximized window switch (msg) { case WM_GETMINMAXINFO: WmGetMinMaxInfo(hwnd, lParam); handled = true; break; case WM_NCCALCSIZE: if (NativeMethods.IsWindowMaximized(hwnd)) { var rc = Marshal.PtrToStructure(lParam); var screen = Screen.FromHandle(hwnd); rc.left = screen.WorkingArea.Left; rc.top = screen.WorkingArea.Top; rc.right = screen.WorkingArea.Right; rc.bottom = screen.WorkingArea.Bottom - 1; // -1 to allow auto-hide taskbar (at the bottom) Marshal.StructureToPtr(rc, lParam, true); } else if (wParam.ToInt32() != 0) { var rc = Marshal.PtrToStructure(lParam); // cheat Windows to force a flicker free resize rc.bottom += 1; Marshal.StructureToPtr(rc, lParam, true); } var retVal = IntPtr.Zero; if (wParam.ToInt32() != 0) { // returning VALIDRECTS | REDRAW gives the smoothest resize behavior retVal = new IntPtr(0x0400 | 0x0100 | 0x0200); } handled = true; return retVal; case WM_NCHITTEST: // required for Win7 (caption buttons issue) handled = true; return new IntPtr(HT_CLIENT); } return (IntPtr)0; } private void WmGetMinMaxInfo(IntPtr hwnd, IntPtr lParam) { var mmi = Marshal.PtrToStructure(lParam); var screen = Screen.FromHandle(hwnd); mmi.ptMaxPosition.X = screen.WorkingArea.Left - screen.Bounds.Left; mmi.ptMaxPosition.Y = screen.WorkingArea.Top - screen.Bounds.Top; mmi.ptMaxSize.X = screen.WorkingArea.Width; mmi.ptMaxSize.Y = screen.WorkingArea.Height - 1; // -1 is a cheat to remove popping effect when maximize window on startup mmi.ptMinTrackSize.X = 50; mmi.ptMinTrackSize.Y = 50; Marshal.StructureToPtr(mmi, lParam, true); } private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = true; } private void CommandBinding_Executed_Minimize(object sender, ExecutedRoutedEventArgs e) { SystemCommands.MinimizeWindow(this); } private void CommandBinding_Executed_Maximize(object sender, ExecutedRoutedEventArgs e) { WindowState = WindowState.Maximized;// SystemCommands.MaximizeWindow(this); // issue with dragging maximized window } private void CommandBinding_Executed_Restore(object sender, ExecutedRoutedEventArgs e) { SystemCommands.RestoreWindow(this); } private void CommandBinding_Executed_Close(object sender, ExecutedRoutedEventArgs e) { SystemCommands.CloseWindow(this); } private void UpdateMaximizeRestoreState() { if (WindowState == WindowState.Maximized) { RestoreButton.Visibility = MaximizeBox ? Visibility.Visible : Visibility.Collapsed; MaximizeButton.Visibility = Visibility.Collapsed; WindowBorder.Visibility = Visibility.Collapsed; ResizingBorder.Visibility = Visibility.Collapsed; NativeMethods.DropShadow(handle, false); } else { RestoreButton.Visibility = Visibility.Collapsed; MaximizeButton.Visibility = MaximizeBox ? Visibility.Visible : Visibility.Collapsed; WindowBorder.Visibility = Visibility.Visible; ResizingBorder.Visibility = Visibility.Visible; NativeMethods.DropShadow(handle, WindowState == WindowState.Normal); } } private SizingAction sizingAction; private void UpdateCursor(FrameworkElement c, SizingAction mode) { var cursor = System.Windows.Input.Cursors.Arrow; switch (mode) { case SizingAction.North: case SizingAction.South: cursor = System.Windows.Input.Cursors.SizeNS; break; case SizingAction.West: case SizingAction.East: cursor = System.Windows.Input.Cursors.SizeWE; break; case SizingAction.NorthWest: case SizingAction.SouthEast: cursor = System.Windows.Input.Cursors.SizeNWSE; break; case SizingAction.NorthEast: case SizingAction.SouthWest: cursor = System.Windows.Input.Cursors.SizeNESW; break; } c.Cursor = cursor; } private void Border_MouseDown(object sender, MouseButtonEventArgs e) { if (e.LeftButton == MouseButtonState.Pressed && sizingAction != SizingAction.None) { NativeMethods.DragSize(handle, sizingAction); e.Handled = true; } } private void Border_MouseMove(object sender, System.Windows.Input.MouseEventArgs e) { var border = sender as Border; var pos = e.GetPosition(border); sizingAction = SizingAction.None; if (pos.X == 0 && pos.Y == 0) return; bool canResize = FormBorderStyle == FormBorderStyle.Sizable || FormBorderStyle == FormBorderStyle.SizableToolWindow; if (canResize && e.LeftButton != MouseButtonState.Pressed) { var width = border.ActualWidth; var height = border.ActualHeight; if (pos.Y <= 4) // top { if (pos.X <= 4) sizingAction = SizingAction.NorthWest; else if (pos.X >= width - 4) sizingAction = SizingAction.NorthEast; else sizingAction = SizingAction.North; } else if (pos.Y >= height - 4) // bottom { if (pos.X <= 4) sizingAction = SizingAction.SouthWest; else if (pos.X >= width - 4) sizingAction = SizingAction.SouthEast; else sizingAction = SizingAction.South; } else // middle { if (pos.X <= 4) sizingAction = SizingAction.West; else if (pos.X >= width - 4) sizingAction = SizingAction.East; } UpdateCursor(border, sizingAction); } } private void Border_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e) { var border = sender as Border; UpdateCursor(border, SizingAction.None); } private bool restoreForDragMove; private void TitleArea_MouseDown(object sender, MouseButtonEventArgs e) { if (e.ClickCount > 1) { if (this.WindowState == WindowState.Maximized) this.WindowState = WindowState.Normal; else if (this.WindowState == WindowState.Normal) this.WindowState = WindowState.Maximized; return; } restoreForDragMove = WindowState == WindowState.Maximized; DragMove(); e.Handled = true; } private void TitleArea_MouseMove(object sender, System.Windows.Input.MouseEventArgs e) { if (restoreForDragMove) { restoreForDragMove = false; var dpiScale = Helper.GetDpiScale(this); var winPoint = e.MouseDevice.GetPosition(this); var scrPoint = PointToScreen(winPoint); scrPoint.X /= dpiScale; scrPoint.Y /= dpiScale; var half = RestoreBounds.Width / 2; if (winPoint.X < half) Left = scrPoint.X - winPoint.X; else if (Width - winPoint.X < half) Left = scrPoint.X - RestoreBounds.Width + (Width - winPoint.X); else Left = scrPoint.X - half; Top = scrPoint.Y - winPoint.Y; WindowState = WindowState.Normal; DragMove(); } } private void TitleArea_MouseUp(object sender, MouseButtonEventArgs e) { restoreForDragMove = false; } private void TitleIcon_MouseDown(object sender, MouseButtonEventArgs e) { if (e.ClickCount > 1) { SystemCommands.CloseWindow(this); e.Handled = true; } else { var pt = this.PointToScreen(new Point(0, TitleArea.ActualHeight)); var scale = Helper.GetDpiScale(this); SystemCommands.ShowSystemMenu(this, new Point(pt.X / scale, pt.Y / scale)); e.Handled = true; } } } }