How do I detect when the WindowState changes?

Most of the time, you can get away with checking it in the Form.SizeChanged or Form.Resize event. If you remember, there's no difference between the two events so it doesnt matter what you pick.

However it was pointed out to me that if you specify a maximum size for your window, this methodology wont work. The actual window message you want to look out for is WM_SIZE (0x05). The wParam of this message gives you details about what's just happened to the window, whether it's been maximized, restored, etc. There is no event that matches this window message, so roll up your sleeves it's WndProc magic hour! [1]

There are a couple of other window messages that correspond to sizing, which get everyone all confused. WM_WINDOWPOSCHANGING and WM_WINDOWPOSCHANGED occur during the sizing of a control - usually in a response to a call to SetWindowPos. The most likely place for windows forms to use SetWindowsPos is when you're actually setting Width, Height, Size, Bounds, Location - this usually all funnels to one virtual method called SetBoundsCore (which I've mentioned how to debug before). 

There is one window message that sneaks in between the WM_WINDOWPOSCHANGING (the I'm about to change size message) and the WM_WINDOWPOSCHANGED (the hey I've changed size message) for toplevel windows - this is WM_GETMINMAXINFO. For toplevel windows with a maximum or minimum size requirement, WM_GETMINMAXINFO is used to apply size restrictions. This is where the Form.MinimumSize and Form.MaximumSize are applied.

In my dealings with these window messages, pretty much these are the only ones you'll care about. Everything else in my experience (like WM_MOVE, etc) seems to be syntatic sugar.  

[1] WndProc Magic hour:
     protected override void WndProc(ref Message m) {
            if (m.Msg == /*WM_SIZE*/ 0x0005) {
                this.Text = this.WindowState.ToString();
            }
            base.WndProc(ref m);
        }