using System; using System.ComponentModel; using System.Drawing; using System.Runtime.InteropServices; using System.Windows.Forms; using OnTopReplica.Native; using WindowsFormsAero.Dwm.Helpers; namespace OnTopReplica { /// /// Form that automatically keeps a certain aspect ratio and resizes without flickering. /// public class AspectRatioForm : GlassForm { bool _keepAspectRatio = true; /// /// Gets or sets whether the form should keep its aspect ratio. /// /// /// Refreshes the window's size if set to true. /// [Description("Enables fixed aspect ratio for this form."), Category("Appearance"), DefaultValue(true)] public bool KeepAspectRatio { get { return _keepAspectRatio; } set { _keepAspectRatio = value; if (value) RefreshAspectRatio(); } } double _aspectRatio = 1.0; /// /// Gets or sets the form's aspect ratio that will be kept automatically when resizing. /// [Description("Determines this form's fixed aspect ratio."), Category("Appearance"), DefaultValue(1.0)] public double AspectRatio { get { return _aspectRatio; } set { if (value <= 0.0 || Double.IsInfinity(value)) return; _aspectRatio = value; } } Padding _extraPadding; /// /// Gets or sets some additional internal padding of the form that is ignored when keeping the aspect ratio. /// [Description("Sets some padding inside the form's client area that is ignored when keeping the aspect ratio."), Category("Appearance")] public Padding ExtraPadding { get { return _extraPadding; } set { _extraPadding = value; if(KeepAspectRatio) RefreshAspectRatio(); } } /// /// Forces the form to update its height based on the current aspect ratio setting. /// public void RefreshAspectRatio() { int newWidth = ClientSize.Width; int newHeight = (int)((ClientSize.Width - ExtraPadding.Horizontal) / AspectRatio) + ExtraPadding.Vertical; //Adapt height if it doesn't respect the form's minimum size Size clientMinimumSize = FromSizeToClientSize(MinimumSize); if (newHeight < clientMinimumSize.Height) { newHeight = clientMinimumSize.Height; newWidth = (int)((newHeight - ExtraPadding.Vertical) * AspectRatio) + ExtraPadding.Horizontal; } //Adapt height if it exceeds the screen's height var workingArea = Screen.GetWorkingArea(this); if (newHeight >= workingArea.Height) { newHeight = workingArea.Height; newWidth = (int)((newHeight - ExtraPadding.Vertical) * AspectRatio) + ExtraPadding.Horizontal; } //Update size ClientSize = new Size(newWidth, newHeight); //Move form vertically to adapt to new size //REMOVED: allows the window to correctly be restored slightly off screen /*if (Location.Y + Size.Height > workingArea.Y + workingArea.Height) { int offsetY = (workingArea.Y + workingArea.Height) - (Location.Y + Size.Height); Location = new Point(Location.X, Location.Y - offsetY); }*/ } /// /// Adjusts the size of the form by a pixel increment while keeping its aspect ratio. /// /// Change of size in pixels. public void AdjustSize(int pixelOffset) { Size origSize = Size; //Resize to new width (clamped to max allowed size and minimum form size) int newWidth = Math.Max(Math.Min(origSize.Width + pixelOffset, SystemInformation.MaxWindowTrackSize.Width), MinimumSize.Width); //Determine new height while keeping aspect ratio var clientConversionDifference = ClientWindowDifference; int newHeight = (int)((newWidth - ExtraPadding.Horizontal - clientConversionDifference.Width) / AspectRatio) + ExtraPadding.Vertical + clientConversionDifference.Height; //Apply and move form to recenter Size = new Size(newWidth, newHeight); int deltaX = Size.Width - origSize.Width; int deltaY = Size.Height - origSize.Height; Location = new System.Drawing.Point(Location.X - (deltaX / 2), Location.Y - (deltaY / 2)); } /// /// Updates the aspect ratio of the form and optionally forces a refresh. /// /// Size from which aspect ratio should be computed. /// True if the size of the form should be refreshed to match the new aspect ratio. public void SetAspectRatio(Size aspectRatioSource, bool forceRefresh) { AspectRatio = ((double)aspectRatioSource.Width / (double)aspectRatioSource.Height); _keepAspectRatio = true; Log.Write("Setting new aspect ratio {0} (for {1})", AspectRatio, aspectRatioSource); if (forceRefresh) { RefreshAspectRatio(); } } #region Event overriding protected override void OnResizeEnd(EventArgs e) { base.OnResizeEnd(e); //Ensure that the ClientSize of the form is always respected //(not ensured by the WM_SIZING message alone because of rounding errors and the chrome space) if (KeepAspectRatio) { var newHeight = (int)Math.Round(((ClientSize.Width - ExtraPadding.Horizontal) / AspectRatio) + ExtraPadding.Vertical); ClientSize = new Size(ClientSize.Width, newHeight); } } /// /// Override WM_SIZING message to restrict resizing. /// Taken from: http://www.vcskicks.com/maintain-aspect-ratio.php /// Improved with code from: http://stoyanoff.info/blog/2010/06/27/resizing-forms-while-keeping-aspect-ratio/ /// protected override void WndProc(ref Message m) { if (KeepAspectRatio && m.Msg == WM.SIZING) { var clientSizeConversion = ClientWindowDifference; var rc = (Native.NRectangle)Marshal.PtrToStructure(m.LParam, typeof(Native.NRectangle)); int res = m.WParam.ToInt32(); int width = (rc.Right - rc.Left) - clientSizeConversion.Width - ExtraPadding.Horizontal; int height = (rc.Bottom - rc.Top) - clientSizeConversion.Height - ExtraPadding.Vertical; if (res == WMSZ.LEFT || res == WMSZ.RIGHT) { //Left or right resize, adjust top and bottom int targetHeight = (int)(width / AspectRatio); int diffHeight = height - targetHeight; rc.Top += (int)(diffHeight / 2.0); rc.Bottom = rc.Top + targetHeight + ExtraPadding.Vertical + clientSizeConversion.Height; } else if (res == WMSZ.TOP || res == WMSZ.BOTTOM) { //Up or down resize, adjust left and right int targetWidth = (int)(height * AspectRatio); int diffWidth = width - targetWidth; rc.Left += (int)(diffWidth / 2.0); rc.Right = rc.Left + targetWidth + ExtraPadding.Horizontal + clientSizeConversion.Width; } else if (res == WMSZ.RIGHT + WMSZ.BOTTOM || res == WMSZ.LEFT + WMSZ.BOTTOM) { //Lower corner resize, adjust bottom rc.Bottom = rc.Top + (int)(width / AspectRatio) + ExtraPadding.Vertical + clientSizeConversion.Height; } else if (res == WMSZ.LEFT + WMSZ.TOP || res == WMSZ.RIGHT + WMSZ.TOP) { //Upper corner resize, adjust top rc.Top = rc.Bottom - (int)(width / AspectRatio) - ExtraPadding.Vertical - clientSizeConversion.Height; } Marshal.StructureToPtr(rc, m.LParam, false); } base.WndProc(ref m); } #endregion #region ClientSize/Size conversion helpers /// /// Converts a client size measurement to a window size measurement. /// /// Size of the window's client area. /// Size of the whole window. public Size FromClientSizeToSize(Size clientSize) { var difference = ClientWindowDifference; return new Size(clientSize.Width + difference.Width, clientSize.Height + difference.Height); } /// /// Converts a window size measurement to a client size measurement. /// /// Size of the whole window. /// Size of the window's client area. public Size FromSizeToClientSize(Size size) { var difference = ClientWindowDifference; return new Size(size.Width - difference.Width, size.Height - difference.Height); } #endregion /// /// Gets the difference in pixels between a client size value and a window size value (depending on window decoration). /// protected Size ClientWindowDifference { get { long style = WindowMethods.GetWindowLong(this.Handle, WindowMethods.WindowLong.Style).ToInt64(); long exStyle = WindowMethods.GetWindowLong(this.Handle, WindowMethods.WindowLong.ExStyle).ToInt64(); return WindowMethods.ConvertClientToWindowRect(new NRectangle(0, 0, 0, 0), style, exStyle).Size; } } } }