Aspect ratio constrained resizing

While working on OnTopReplica I needed a way to constrain the aspect ratio of the "cloned" window. Since the live thumbnails created using the DWM API cannot be manipulated and always maintain the aspect of the original window, it makes sense to limit the user's choices in resizing the window, in order to fit the window to the cloned thumbnail automatically.

Much like in the case of media players, where video files have a certain aspect ratio (let's ignore that this ratio can usually be changed by the user). If the video doesn't fit inside the player window, black bars are added on top and bottom or on the sides. Limiting the user's resizing options in this case actually improves the interface experience because a tedious task is handled directly (like searching the right size of the window to reduce the black bars).

In Windows Forms you have two options to react to resize events of the window: the OnResize event and the couple OnResizeBegin and OnResizeEnd. The problem of those two methods is that they both run after the window has been resized. That is, after the window received the WM_SIZE message (in Win32 terms). You can force the window to adopt a correct size after receiving one of those events.

This would work, if it weren't for the option "Show window contents while resizing" (added a long way back in Windows 98, more or less). That option (which is usually on by default) causes the window to generate a lot of WM_SIZE messages while it is being resized. If you react by changing the size on each event, the result is an extremely jerky window that jumps back and forth while the users tries to resize it. And the contents of the window flicker awfully as a result.

The WM_SIZING message

The solution is pretty easy, if you know where to search. I found out only recently, by reading this article. Windows actually fires a WM_SIZING message before resizing the window. This message contains a reference to the "final" window size that can be freely altered to match the desired aspect ratio. In this way, when the WM_SIZE message is sent, the window size has already been changed to the correct one and no ugly flickering occurs.

The code I used for OnTopReplica is as follows:

protected override void WndProc(ref Message m) {
	if (m.Msg == NativeMethods.WM_SIZING) {
		//This is the rectangle to which the window would resize normally
		var rc = (NativeMethods.Rectangle)Marshal.PtrToStructure(m.LParam,
			typeof(NativeMethods.Rectangle));
		//This is the resizing direction
		int res = m.WParam.ToInt32();

		if (res == NativeMethods.WMSZ_LEFT || res == NativeMethods.WMSZ_RIGHT) {
			//Adjust height and vertical position
			int targetHeight = (int)(this.Width / AspectRatio);
			int originalHeight = rc.Bottom - rc.Top;
			int diffHeight = originalHeight - targetHeight;

			rc.Top += (diffHeight / 2);
			rc.Bottom = rc.Top + targetHeight;
		}
		else if (res == NativeMethods.WMSZ_TOP || res == NativeMethods.WMSZ_BOTTOM) {
			//Adjust width and horizontal position
			int targetWidth = (int)(this.Height * AspectRatio);
			int originalWidth = rc.Right - rc.Left;
			int diffWidth = originalWidth - targetWidth;

			rc.Left += (diffWidth / 2);
			rc.Right = rc.Left + targetWidth;
		}
		else if (res == NativeMethods.WMSZ_RIGHT + NativeMethods.WMSZ_BOTTOM) {
			//Lower-right corner
			rc.Bottom = rc.Top + (int)(this.Width / AspectRatio);
		}
		else if (res == NativeMethods.WMSZ_LEFT + NativeMethods.WMSZ_BOTTOM) {
			//Lower-left corner
			rc.Bottom = rc.Top + (int)(this.Width / AspectRatio);
		}
		else if (res == NativeMethods.WMSZ_LEFT + NativeMethods.WMSZ_TOP) {
			//Upper-left corner
			rc.Left = rc.Right - (int)(this.Height * AspectRatio);
		}
		else if (res == NativeMethods.WMSZ_RIGHT + NativeMethods.WMSZ_TOP) {
			//Upper-right corner
			rc.Right = rc.Left + (int)(this.Height * AspectRatio);
		}

		Marshal.StructureToPtr(rc, m.LParam, true);
	}

	base.WndProc(ref m);
}
Aspect ratio constrained resizing.
Resizing the window vertically.

The only slightly more complex code is used for exclusively horizontal or vertical resize operations. If the user is resizing the window vertically, you should not only adjust the window width to match the intended aspect ratio, but also the window's horizontal position: in this way the window will appear like docked at the top (or bottom) center.

Here's the source code for a Windows.Forms.Form subclass that automatically maintains its aspect ratio.