Related
I want to change form size depending on Screen and it's resolution.
What I want is a correct event to track these screen changes as well as screen resolution changes at runtime.
In other words,
If user is using two screens and move application to another screen, that should be tracked and change size accordingly, i.e. reduce size if new screen's resolution is low or increase size if resolution is larger.
Also track screen resolution change on the same screen, and make changes to size accordingly.
I know how to change Form size, get current screen and it's resolution, just need these events to keep track of these changes.
Going over this answer I've decided to improve it and add further information to form a more complete solution.
The Challenge
Tracking which screen a Form is currently being rendered on. This can change if a user drags the form to another monitor or unplugs a monitor. The resolution can change if a user manually drags a window to a different display or changes the resolution directly.
Firstly, tracking form location. We need to hook into a Move event for the form context, fortunately the .Net framework provides such an event, and it is named Control.Move Event.
Secondly, we will need to hook into a screen resolution changed event, we can do this with the SystemEvents.DisplaySettingsChanged event.
And putting it together, I got this:
struct Resolution
{
public int Width;
public int Height;
}
int previous = -1;
int current = -1;
private bool CheckScreenChanged()
{
bool changed = false;
current = GetScreenIndex();
if (current != -1 && previous != -1 && current != previous) // form changed screen.
{
changed = true;
}
previous = current;
return changed;
}
private int GetScreenIndex()
{
return Array.IndexOf(Screen.AllScreens, Screen.FromControl(this));
}
private Resolution GetCurrentResolution()
{
Screen screen = Screen.FromControl(this);
Resolution res = new Resolution();
res.Width = screen.Bounds.Width;
res.Height = screen.Bounds.Height;
return res;
}
private void SetResolutionLabel()
{
Resolution res = GetCurrentResolution();
label2.Text = String.Format("Width: {0}, Height: {1}", res.Width, res.Height);
}
private void ScreenChanged()
{
label1.Text = "Screen " + current.ToString();
}
private void Form_Moved(object sender, System.EventArgs e)
{
bool changed = CheckScreenChanged();
if (changed == true)
{
ScreenChanged();
SetResolutionLabel();
}
}
public void SystemEvents_DisplaySettingsChanged(object sender, EventArgs e)
{
SetResolutionLabel();
}
public void Initialize()
{
this.Move += Form_Moved;
SystemEvents.DisplaySettingsChanged += new
EventHandler(SystemEvents_DisplaySettingsChanged);
previous = GetScreenIndex();
current = GetScreenIndex();
ScreenChanged();
SetResolutionLabel();
}
The code above is tested on a simple form with two labels called label1 and label2, which are updated when the screen the form is on changes or the resolution changes.
An image of this in action on my primary screen/display
And on my secondary screen/display when the form has been dragged to it:
I've got a telerik radchart that's displaying a beautiful and incredibly-useful-to-end-user line chart.
One of the requirements for this chart was to have a x-value crosshair on the graph (e.g. when a user hovers at any point over the graph, a horizontal line appears, and where this line intersects the actual graph line, a value is displayed in another area of the screen).
In a previous iteration, I used flotJS to do my graphing & crosshair'ing, worked great, and was blazing fast. In converting this to silverlight, I've seen a tremendous amount of lag, and I'd like to know if anyone has any ideas on improving performance.
I currently expose the chart's MouseEnter/MouseLeave events to hide/show the horizontal line. This is done via a System.Windows.Visibility property sitting in my viewmodel (which my crosshair's visibility property is bound to). I then use the MouseMove method to actually calculate the x position.
Here is the code that runs this (first in my chart view's code behind)
private void rad_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
{
var plotAreaPanel = this.rad.DefaultView.ChartArea.ChildrenOfType<ClipPanel>().FirstOrDefault();
var position = e.GetPosition(plotAreaPanel);
var x = rad.DefaultView.ChartArea.AxisX.ConvertPhysicalUnitsToData(position.X);
(this.DataContext as ViewModels.DateCountsViewModel).XVolume = x;
}
private void rad_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
{
(this.DataContext as ViewModels.DateCountsViewModel).XVolumeVisibility = System.Windows.Visibility.Visible;
}
private void rad_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
{
(this.DataContext as ViewModels.DateCountsViewModel).XVolumeVisibility = System.Windows.Visibility.Collapsed;
}
Then in my viewmodel, in the setter for XVolume (which is being Set in the MouseMove method above), this code:
public double XVolume
{
get { return xVolume; }
set
{
this.xVolume = value;
decimal currentX = (decimal)Math.Round(value, 0);
var vol = this.VolumeCollection.Where(x => x.XValue == currentX).FirstOrDefault() as Models.ChartModel;
this.Volume = (vol == null) ? 0 : (int)vol.YValue;
RaisePropertyChanged("XVolume");
}
}
With heavy usage, the line lags up to several inches behind my pointer. I tried throttling my mouse events outlined here, but it's still really laggy.
[Note regarding link above: It uses the CompositionTarget.Rendering event, which is fired just before a frame is rendered. Therefore, to throttle the effect of the mouse event, we set a flag to indicate that we are waiting for a change to be rendered, then reset this flag just before rendering occurs... not allowing simultaneous rendering]
What can I do to mitigate this lag?
Many posts around about restoring a WinForm position and size.
Examples:
www.stackoverflow.com/questions/92540/save-and-restore-form-position-and-size
www.codeproject.com/KB/dialog/restoreposition.aspx?fid=1249382&df=90&mpp=25&noise=3&sort=Position&view=Quick&select=2595746
But I have yet to find code to do this with multiple monitors.
That is, if I close my .NET Winform app with the window on monitor 2, I want it to save the windows size, location, and state to the application settings, so it could later restore to monitor 2 when I restart the app. It would be nice if, like in the codeproject example above, it includes some sanity checks, as in if the saved location is mostly off-screen it "fixes" it. Or if the saved location is on a monitor that is no longer there (e.g. my laptop is now by itself without my second monitor) then it correctly moves it to monitor 1.
Any thoughts?
My environment: C#, .NET 3.5 or below, VS2008
Try this code. Points of interest:
Checks if the window is (partially) visible on any screen's working area. E.g. dragging it behind the task bar or moving it completely offscreen resets the position to windows default.
Saves the correct bounds even if the Form is minimized or maximized (common error)
Saves the WindowState correctly. Saving FormWindowState.Minimized is disabled by design.
The bounds and state are stored in the appsettings with their corresponding type so there's no need to do any string parsing. Let the framework do its serialization magic.
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
// this is the default
this.WindowState = FormWindowState.Normal;
this.StartPosition = FormStartPosition.WindowsDefaultBounds;
// check if the saved bounds are nonzero and visible on any screen
if (Settings.Default.WindowPosition != Rectangle.Empty &&
IsVisibleOnAnyScreen(Settings.Default.WindowPosition))
{
// first set the bounds
this.StartPosition = FormStartPosition.Manual;
this.DesktopBounds = Settings.Default.WindowPosition;
// afterwards set the window state to the saved value (which could be Maximized)
this.WindowState = Settings.Default.WindowState;
}
else
{
// this resets the upper left corner of the window to windows standards
this.StartPosition = FormStartPosition.WindowsDefaultLocation;
// we can still apply the saved size
this.Size = Settings.Default.WindowPosition.Size;
}
}
private bool IsVisibleOnAnyScreen(Rectangle rect)
{
foreach (Screen screen in Screen.AllScreens)
{
if (screen.WorkingArea.IntersectsWith(rect))
{
return true;
}
}
return false;
}
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
// only save the WindowState if Normal or Maximized
switch (this.WindowState)
{
case FormWindowState.Normal:
case FormWindowState.Maximized:
Settings.Default.WindowState = this.WindowState;
break;
default:
Settings.Default.WindowState = FormWindowState.Normal;
break;
}
// reset window state to normal to get the correct bounds
// also make the form invisible to prevent distracting the user
this.Visible = false;
this.WindowState = FormWindowState.Normal;
Settings.Default.WindowPosition = this.DesktopBounds;
Settings.Default.Save();
}
}
The settings file for reference:
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="ScreenTest" GeneratedClassName="Settings">
<Profiles />
<Settings>
<Setting Name="WindowPosition" Type="System.Drawing.Rectangle" Scope="User">
<Value Profile="(Default)">0, 0, 0, 0</Value>
</Setting>
<Setting Name="WindowState" Type="System.Windows.Forms.FormWindowState" Scope="User">
<Value Profile="(Default)">Normal</Value>
</Setting>
</Settings>
</SettingsFile>
The answer provided by VVS was a great help! I found two minor issues with it though, so I am reposting the bulk of his code with these revisions:
(1) The very first time the application runs, the form is opened in a Normal state but is sized such that it appears as just a title bar. I added a conditional in the constructor to fix this.
(2) If the application is closed while minimized or maximized the code in OnClosing fails to remember the dimensions of the window in its Normal state. (The 3 lines of code--which I have now commented out--seems reasonable but for some reason just does not work.) Fortunately I had previously solved this problem and have included that code in a new region at the end of the code to track window state as it happens rather than wait for closing.
With these two fixes in place, I have tested:
A. closing in normal state--restores to same size/position and state
B. closing in minimized state--restores to normal state with last normal size/position
C. closing in maximized state--restores to maximized state and remembers its last size/position when one later adjusts to normal state.
D. closing on monitor 2--restores to monitor 2.
E. closing on monitor 2 then disconnecting monitor 2--restores to same position on monitor 1
David: your code allowed me to achieve points D and E almost effortlessly--not only did you provide a solution for my question, you provided it in a complete program so I had it up and running almost within seconds of pasting it into Visual Studio. So a big thank you for that!
public partial class MainForm : Form
{
bool windowInitialized;
public MainForm()
{
InitializeComponent();
// this is the default
this.WindowState = FormWindowState.Normal;
this.StartPosition = FormStartPosition.WindowsDefaultBounds;
// check if the saved bounds are nonzero and visible on any screen
if (Settings.Default.WindowPosition != Rectangle.Empty &&
IsVisibleOnAnyScreen(Settings.Default.WindowPosition))
{
// first set the bounds
this.StartPosition = FormStartPosition.Manual;
this.DesktopBounds = Settings.Default.WindowPosition;
// afterwards set the window state to the saved value (which could be Maximized)
this.WindowState = Settings.Default.WindowState;
}
else
{
// this resets the upper left corner of the window to windows standards
this.StartPosition = FormStartPosition.WindowsDefaultLocation;
// we can still apply the saved size
// msorens: added gatekeeper, otherwise first time appears as just a title bar!
if (Settings.Default.WindowPosition != Rectangle.Empty)
{
this.Size = Settings.Default.WindowPosition.Size;
}
}
windowInitialized = true;
}
private bool IsVisibleOnAnyScreen(Rectangle rect)
{
foreach (Screen screen in Screen.AllScreens)
{
if (screen.WorkingArea.IntersectsWith(rect))
{
return true;
}
}
return false;
}
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
// only save the WindowState if Normal or Maximized
switch (this.WindowState)
{
case FormWindowState.Normal:
case FormWindowState.Maximized:
Settings.Default.WindowState = this.WindowState;
break;
default:
Settings.Default.WindowState = FormWindowState.Normal;
break;
}
# region msorens: this code does *not* handle minimized/maximized window.
// reset window state to normal to get the correct bounds
// also make the form invisible to prevent distracting the user
//this.Visible = false;
//this.WindowState = FormWindowState.Normal;
//Settings.Default.WindowPosition = this.DesktopBounds;
# endregion
Settings.Default.Save();
}
# region window size/position
// msorens: Added region to handle closing when window is minimized or maximized.
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
TrackWindowState();
}
protected override void OnMove(EventArgs e)
{
base.OnMove(e);
TrackWindowState();
}
// On a move or resize in Normal state, record the new values as they occur.
// This solves the problem of closing the app when minimized or maximized.
private void TrackWindowState()
{
// Don't record the window setup, otherwise we lose the persistent values!
if (!windowInitialized) { return; }
if (WindowState == FormWindowState.Normal)
{
Settings.Default.WindowPosition = this.DesktopBounds;
}
}
# endregion window size/position
}
Most of the other solutions here rely on manually figuring out the current positioning of each monitor. The edge cases are extremely difficult to figure out, and very few apps can get it right rolling their own.
The SetWindowPlacement function within Windows itself correctly handles all of the edge cases - if the window would be positioned off of a visible screen, it adjusts it accordingly.
The best example I've seen in C# is on David Rickard's blog. Not only does it show how to use SetWindowPlacement, it also shows how to serialize the entire result.
http://blogs.msdn.com/b/davidrickard/archive/2010/03/09/saving-window-size-and-location-in-wpf-and-winforms.aspx
This is the perfect one I think based on your answers and comments.
This solution is to save/restore form size and position with multi monitors + multi document, multi form or multi main form support. It is not MDI form but Microsoft Word like multi document with different main form instance.
Thanks to VVS, msorens and Ian Goldby. I merge the solution from VVS, msorens and MSDN Application.Run Method (ApplicationContext) example to make the multi MainForm but not MDI.
This fix of includes the comment from Ian Goldby which uses Form.RestoreBounds to eliminate OnResize(), OnMove() and TrackWindowState().
I also fix to remember the Monitor when the Form move to the other Monitor and getting maximized before exit because I not tracking the OnResize, OnMove.
By this fix, this solution support Windows 7 Snap feature which you can drag the titlebar or Win+Arrow key to snap Form window into any monitor edge or make it maximized/normal as well as minimized.
This solution implemented in Program but not in the main Form to support multi main Form. However you can use for single main Form also.
using System;
using System.Collections.Generic;
using System.Windows.Forms;
using SimpleTestForm.Properties;
using System.Drawing;
namespace SimpleTestForm
{
static class Program
{
static MultiMainFormAppContext appContext;
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
appContext = new MultiMainFormAppContext();
Application.Run(appContext);
}
/// <summary>
/// Create a new MainForm and restore the form size and position if necessary. This method can be called like from Menu File > New click event.
/// </summary>
/// <returns></returns>
public static MainForm createNewMainForm()
{
return appContext.createNewMainForm();
}
/// <summary>
/// Get the current active MainForm event if a dialog is opened. Useful to create Dictionary (MainForm, T) to store Form/document dependent field. Please set the Owner of child form to prevent null reference exception.
/// </summary>
/// <returns></returns>
public static MainForm GetCurrentMainFormInstance()
{
Form mainForm = Form.ActiveForm;
while (!(mainForm is MainForm) && mainForm.Owner != null)
mainForm = mainForm.Owner;
return mainForm as MainForm;
}
}
class MultiMainFormAppContext : ApplicationContext
{
List<MainForm> mainForms = new List<MainForm>();
Point newRestoredLocation = Point.Empty;
internal MultiMainFormAppContext()
{
createNewMainForm();
}
internal MainForm createNewMainForm()
{
MainForm mainForm = new MainForm();
mainForm.FormClosed += new FormClosedEventHandler(mainForm_FormClosed);
mainForm.LocationChanged += new EventHandler(mainForm_LocationChanged);
RestoreFormSizeNPosition(mainForm);
PreventSameLocation(mainForm);
mainForms.Add(mainForm);
mainForm.Show();
return mainForm;
}
private void PreventSameLocation(MainForm mainForm)
{
const int distance = 20;
foreach (MainForm otherMainForm in mainForms)
{
if (Math.Abs(otherMainForm.Location.X - mainForm.Location.X) < distance &&
Math.Abs(otherMainForm.Location.Y - mainForm.Location.Y) < distance)
mainForm.Location = new Point(mainForm.Location.X + distance, mainForm.Location.Y + distance);
}
}
/// <summary>
/// Restore the form size and position with multi monitor support.
/// </summary>
private void RestoreFormSizeNPosition(MainForm mainForm)
{
// this is the default
mainForm.WindowState = FormWindowState.Normal;
mainForm.StartPosition = FormStartPosition.WindowsDefaultBounds;
// check if the saved bounds are nonzero and visible on any screen
if (Settings.Default.WindowPosition != Rectangle.Empty &&
IsVisibleOnAnyScreen(Settings.Default.WindowPosition))
{
// first set the bounds
mainForm.StartPosition = FormStartPosition.Manual;
mainForm.DesktopBounds = Settings.Default.WindowPosition;
// afterwards set the window state to the saved value (which could be Maximized)
mainForm.WindowState = Settings.Default.WindowState;
}
else
{
// this resets the upper left corner of the window to windows standards
mainForm.StartPosition = FormStartPosition.WindowsDefaultLocation;
// we can still apply the saved size if not empty
if (Settings.Default.WindowPosition != Rectangle.Empty)
{
mainForm.Size = Settings.Default.WindowPosition.Size;
}
}
}
private void SaveFormSizeNPosition(MainForm mainForm)
{
// only save the WindowState as Normal or Maximized
Settings.Default.WindowState = FormWindowState.Normal;
if (mainForm.WindowState == FormWindowState.Normal || mainForm.WindowState == FormWindowState.Maximized)
Settings.Default.WindowState = mainForm.WindowState;
if (mainForm.WindowState == FormWindowState.Normal)
{
Settings.Default.WindowPosition = mainForm.DesktopBounds;
}
else
{
if (newRestoredLocation == Point.Empty)
Settings.Default.WindowPosition = mainForm.RestoreBounds;
else
Settings.Default.WindowPosition = new Rectangle(newRestoredLocation, mainForm.RestoreBounds.Size);
}
Settings.Default.Save();
}
private bool IsVisibleOnAnyScreen(Rectangle rect)
{
foreach (Screen screen in Screen.AllScreens)
{
if (screen.WorkingArea.IntersectsWith(rect))
return true;
}
return false;
}
void mainForm_LocationChanged(object sender, EventArgs e)
{
MainForm mainForm = sender as MainForm;
if (mainForm.WindowState == FormWindowState.Maximized)
{
// get the center location of the form incase like RibbonForm will be bigger and maximized Location wll be negative value that Screen.FromPoint(mainForm.Location) will going to the other monitor resides on the left or top of primary monitor.
// Another thing, you might consider the form is in the monitor even if the location (top left corner) is on another monitor because majority area is on the monitor, so center point is the best way.
Point centerFormMaximized = new Point (mainForm.DesktopBounds.Left + mainForm.DesktopBounds.Width/2, mainForm.DesktopBounds.Top + mainForm.DesktopBounds.Height/2);
Point centerFormRestored = new Point(mainForm.RestoreBounds.Left + mainForm.RestoreBounds.Width / 2, mainForm.RestoreBounds.Top + mainForm.RestoreBounds.Height / 2);
Screen screenMaximized = Screen.FromPoint(centerFormMaximized);
Screen screenRestored = Screen.FromPoint(centerFormRestored);
// we need to change the Location of mainForm.RestoreBounds to the new screen where the form currently maximized.
// RestoreBounds does not update the Location if you change the screen but never restore to FormWindowState.Normal
if (screenMaximized.DeviceName != screenRestored.DeviceName)
{
newRestoredLocation = mainForm.RestoreBounds.Location;
int screenOffsetX = screenMaximized.Bounds.Location.X - screenRestored.Bounds.Location.X;
int screenOffsetY = screenMaximized.Bounds.Location.Y - screenRestored.Bounds.Location.Y;
newRestoredLocation.Offset(screenOffsetX, screenOffsetY);
return;
}
}
newRestoredLocation = Point.Empty;
}
void mainForm_FormClosed(object sender, FormClosedEventArgs e)
{
MainForm mainForm = sender as MainForm;
SaveFormSizeNPosition(mainForm);
mainForm.FormClosed -= new FormClosedEventHandler(mainForm_FormClosed);
mainForm.LocationChanged -= new EventHandler(mainForm_LocationChanged);
mainForm.Dispose();
mainForms.Remove(mainForm);
if (mainForms.Count == 0) ExitThread();
}
}
}
Edit: PreventSameLocation method added to make sure the 2nd form opened not exactly on top of the 1st form and user will notice the newly opened form.
If you have multiple monitors, I believe the screen UI dimensions are simply larger. So the normal "1 monitor" approach of storing and restoring the location will just work. I haven't tried this because I am away from my second monitor but it shouldn't be hard to test. The way you asked the Question is seems like you haven't tested it.
Your second requirement will mean you will have to check the max sceen dimensions when restoring the app, and then reposition as necessary. To do this latter bit, I use this code:
private System.Drawing.Rectangle ConstrainToScreen(System.Drawing.Rectangle bounds)
{
Screen screen = Screen.FromRectangle(bounds);
System.Drawing.Rectangle workingArea = screen.WorkingArea;
int width = Math.Min(bounds.Width, workingArea.Width);
int height = Math.Min(bounds.Height, workingArea.Height);
// mmm....minimax
int left = Math.Min(workingArea.Right - width, Math.Max(bounds.Left, workingArea.Left));
int top = Math.Min(workingArea.Bottom - height, Math.Max(bounds.Top, workingArea.Top));
return new System.Drawing.Rectangle(left, top, width, height);
}
I call this method when restoring the form. I store the screen geometry in the registry on form close, and then read the geometry on form open. I get the bounds, but then constrain the restored bounds to the actual current screen, using the method above.
Save on close:
// store the size of the form
int w = 0, h = 0, left = 0, top = 0;
if (this.Bounds.Width < this.MinimumSize.Width || this.Bounds.Height < this.MinimumSize.Height)
{
// The form is currently minimized.
// RestoreBounds is the size of the window prior to last minimize action.
w = this.RestoreBounds.Width;
h = this.RestoreBounds.Height;
left = this.RestoreBounds.Location.X;
top = this.RestoreBounds.Location.Y;
}
else
{
w = this.Bounds.Width;
h = this.Bounds.Height;
left = this.Location.X;
top = this.Location.Y;
}
AppCuKey.SetValue(_rvn_Geometry,
String.Format("{0},{1},{2},{3},{4}",
left, top, w, h, (int)this.WindowState));
Restore on form open:
// restore the geometry of the form
string s = (string)AppCuKey.GetValue(_rvn_Geometry);
if (!String.IsNullOrEmpty(s))
{
int[] p = Array.ConvertAll<string, int>(s.Split(','),
new Converter<string, int>((t) => { return Int32.Parse(t); }));
if (p != null && p.Length == 5)
this.Bounds = ConstrainToScreen(new System.Drawing.Rectangle(p[0], p[1], p[2], p[3]));
}
This is an old question, but here is a VB version based on the previous answers.
One problem with the answers suggested by VVS and Michael Sorens is that a saved position that only shows a couple of pixels on a screen counts as visible. This solution requires at least 50x50 pixels in intersection before restoring the previous location.
Settings:
<Settings>
<Setting Name="WindowState" Type="System.Windows.Forms.FormWindowState" Scope="User">
<Value Profile="(Default)">Normal</Value>
</Setting>
<Setting Name="WindowBounds" Type="System.Drawing.Rectangle" Scope="User">
<Value Profile="(Default)">10, 10, 800, 600</Value>
</Setting>
</Settings>
Form:
Partial Public Class MainForm
Private loadingComplete As Boolean = False
Public Sub New()
InitializeComponent()
RestoreWindowLocation()
End Sub
Private Sub MainForm_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
loadingComplete = True
End Sub
Private Sub MainForm_Resize(sender As System.Object, e As System.EventArgs) Handles MyBase.Resize
TrackWindowLocation()
End Sub
Private Sub MainForm_Move(sender As System.Object, e As System.EventArgs) Handles MyBase.Move
TrackWindowLocation()
End Sub
Private Sub MainForm_FormClosing(sender As System.Object, e As System.Windows.Forms.FormClosingEventArgs) Handles MyBase.FormClosing
SaveWindowLocation()
End Sub
Private Sub RestoreWindowLocation()
If IsRectangleVisible(My.Settings.WindowBounds) Then
Me.StartPosition = FormStartPosition.Manual
Me.DesktopBounds = My.Settings.WindowBounds
End If
If Not My.Settings.WindowState = FormWindowState.Minimized Then
Me.WindowState = My.Settings.WindowState
End If
End Sub
Private Sub TrackWindowLocation()
If loadingComplete Then
If Me.WindowState = FormWindowState.Normal Then
My.Settings.WindowBounds = Me.DesktopBounds
My.Settings.WindowState = Me.WindowState
End If
End If
End Sub
Private Sub SaveWindowLocation()
If Not Me.WindowState = FormWindowState.Minimized Then
My.Settings.WindowState = Me.WindowState
End If
If Me.WindowState = FormWindowState.Normal Then
My.Settings.WindowBounds = Me.DesktopBounds
End If
My.Settings.Save()
End Sub
Private Function IsRectangleVisible(Rectangle As Rectangle) As Boolean
For Each screen As Screen In screen.AllScreens
Dim r As Rectangle = Rectangle.Intersect(Rectangle, screen.WorkingArea)
If Not r.IsEmpty Then
If r.Width > 50 And r.Height > 50 Then Return True
End If
Next
Return False
End Function
End Class
It seems like a standard requirement: next time the user launches the application, open the window in the same position and state as it was before. Here's my wish list:
Window position same as it was
Unless the screen has resized and the old position is now off screen.
Splitters should retain their position
Tab containers should retain their selection
Some dropdowns should retain their selection
Window state (maximize, minimize, normal) is the same as it was.
Maybe you should never start minimized, I haven't decided.
I'll add my current solutions as an answer along with the limitations.
My other option is to write more custom code around the application settings and execute it on formLoad and formClosed. This doesn't use data binding.
Drawbacks:
More code to write.
Very fiddly. The order you set the properties on formLoad is confusing. For example, you have to make sure you've set the window size before you set the splitter distance.
Right now, this is my preferred solution, but it seems like too much work. To reduce the work, I created a WindowSettings class that serializes the window location, size, state, and any splitter positions to a single application setting. Then I can just create a setting of that type for each form in my application, save on close, and restore on load.
I posted the source code, including the WindowSettings class and some forms that use it. Instructions on adding it to a project are included in the WindowSettings.cs file. The trickiest part was figuring out how to add an application setting with a custom type. You choose Browse... from the type dropdown, and then manually enter the namespace and class name. Types from your project don't show up in the list.
Update: I added some static methods to simplify the boilerplate code that you add to each form. Once you've followed the instructions for adding the WindowSettings class to your project and creating an application setting, here's an example of the code that has to be added to each form whose position you want to record and restore.
private void MyForm_FormClosing(object sender, FormClosingEventArgs e)
{
Settings.Default.CustomWindowSettings = WindowSettings.Record(
Settings.Default.CustomWindowSettings,
this,
splitContainer1);
}
private void MyForm_Load(object sender, EventArgs e)
{
WindowSettings.Restore(
Settings.Default.CustomWindowSettings,
this,
splitContainer1);
}
The sample below shows how I do it
SavePreferences is called when closing the form and saves the form's size, and a flag indicating if it's maximized (in this version I don't save if it's minimized - it will come back up restored or maximized next time).
LoadPreferences is called from OnLoad.
First save the design-time WindowState and set it to Normal. You can only successfully set the form size if it's WindowState is Normal.
Next restore the Size from your persisted settings.
Now make sure the form fits on your screen (call to FitToScreen). The screen resolution may have changed since you last ran the application.
Finally set the WindowState back to Maximized (if persisted as such), or to the design-time value saved earlier.
This could obviously be adapted to persist the start position and whether the form was minimized when closed - I didn't need to do that. Other settings for controls on your form such as splitter position and tab container are straightforward.
private void FitToScreen()
{
if (this.Width > Screen.PrimaryScreen.WorkingArea.Width)
{
this.Width = Screen.PrimaryScreen.WorkingArea.Width;
}
if (this.Height > Screen.PrimaryScreen.WorkingArea.Height)
{
this.Height = Screen.PrimaryScreen.WorkingArea.Height;
}
}
private void LoadPreferences()
{
// Called from Form.OnLoad
// Remember the initial window state and set it to Normal before sizing the form
FormWindowState initialWindowState = this.WindowState;
this.WindowState = FormWindowState.Normal;
this.Size = UserPreferencesManager.LoadSetting("_Size", this.Size);
_currentFormSize = Size;
// Fit to the current screen size in case the screen resolution
// has changed since the size was last persisted.
FitToScreen();
bool isMaximized = UserPreferencesManager.LoadSetting("_Max", initialWindowState == FormWindowState.Maximized);
WindowState = isMaximized ? FormWindowState.Maximized : FormWindowState.Normal;
}
private void SavePreferences()
{
// Called from Form.OnClosed
UserPreferencesManager.SaveSetting("_Size", _currentFormSize);
UserPreferencesManager.SaveSetting("_Max", this.WindowState == FormWindowState.Maximized);
... save other settings
}
x
The simplest solution I've found is to use data binding with the application settings. I bind the location and clientSize properties on the window along with the splitterDistance on the splitter.
Drawbacks:
If you close the window while minimized, it opens hidden the next time. It's really hard to get the window back.
If you close the window while maximized, it opens filling the whole screen, but not maximized (minor issue).
Resizing the window using the top-right corner or the bottom-left corner is just ugly. I guess the two databound properties are fighting each other.
If you'd like to experiment with the strange behaviour, I posted a sample solution using this technique.
I make a Setting for each value I want to save, and use code like this:
private void MainForm_Load(object sender, EventArgs e) {
RestoreState();
}
private void MainForm_FormClosing(object sender, FormClosingEventArgs e) {
SaveState();
}
private void SaveState() {
if (WindowState == FormWindowState.Normal) {
Properties.Settings.Default.MainFormLocation = Location;
Properties.Settings.Default.MainFormSize = Size;
} else {
Properties.Settings.Default.MainFormLocation = RestoreBounds.Location;
Properties.Settings.Default.MainFormSize = RestoreBounds.Size;
}
Properties.Settings.Default.MainFormState = WindowState;
Properties.Settings.Default.SplitterDistance = splitContainer1.SplitterDistance;
Properties.Settings.Default.Save();
}
private void RestoreState() {
if (Properties.Settings.Default.MainFormSize == new Size(0, 0)) {
return; // state has never been saved
}
StartPosition = FormStartPosition.Manual;
Location = Properties.Settings.Default.MainFormLocation;
Size = Properties.Settings.Default.MainFormSize;
// I don't like an app to be restored minimized, even if I closed it that way
WindowState = Properties.Settings.Default.MainFormState ==
FormWindowState.Minimized ? FormWindowState.Normal : Properties.Settings.Default.MainFormState;
splitContainer1.SplitterDistance = Properties.Settings.Default.SplitterDistance;
}
Keep in mind that recompiling wipes the config file where the settings are stored, so test it without making code changes in between a save and a restore.
Based on the accepted answer by Don Kirkby and the WindowSettings class he wrote, you could derive a CustomForm from the standard one to reduce the amount of identical code written for each and every form, maybe like this:
using System;
using System.Configuration;
using System.Reflection;
using System.Windows.Forms;
namespace CustomForm
{
public class MyCustomForm : Form
{
private ApplicationSettingsBase _appSettings = null;
private string _settingName = "";
public Form() : base() { }
public Form(ApplicationSettingsBase settings, string settingName)
: base()
{
_appSettings = settings;
_settingName = settingName;
this.Load += new EventHandler(Form_Load);
this.FormClosing += new FormClosingEventHandler(Form_FormClosing);
}
private void Form_Load(object sender, EventArgs e)
{
if (_appSettings == null) return;
PropertyInfo settingProperty = _appSettings.GetType().GetProperty(_settingName);
if (settingProperty == null) return;
WindowSettings previousSettings = settingProperty.GetValue(_appSettings, null) as WindowSettings;
if (previousSettings == null) return;
previousSettings.Restore(this);
}
private void Form_FormClosing(object sender, FormClosingEventArgs e)
{
if (_appSettings == null) return;
PropertyInfo settingProperty = _appSettings.GetType().GetProperty(_settingName);
if (settingProperty == null) return;
WindowSettings previousSettings = settingProperty.GetValue(_appSettings, null) as WindowSettings;
if (previousSettings == null)
previousSettings = new WindowSettings();
previousSettings.Record(this);
settingProperty.SetValue(_appSettings, previousSettings, null);
_appSettings.Save();
}
}
}
To use this, pass your application settings class and setting name in the constructor:
CustomForm.MyCustomForm f = new CustomForm.MyCustomForm(Properties.Settings.Default, "formSettings");
This uses Reflection to get/set the previous settings from/to the settings class. It may not be optimal to put the Save call into the Form_Closing routine, one could remove that and save the settings file whenever the main app exits.
To use it as a regular form, just use the parameterless constructor.
You can use the application settings to set which control properties will be persisted, in the Form_closed event you have to use the save method on the application settings to write these to disk:
Properties.Settings.Default.Save();
Here is an example of a few I use myself. It only takes into consideration the primary monitor, so it might be better to handle it differently if used on multiple monitors.
Size size;
int x;
int y;
if (WindowState.Equals(FormWindowState.Normal))
{
size = Size;
if (Location.X + size.Width > Screen.PrimaryScreen.Bounds.Width)
x = Screen.PrimaryScreen.Bounds.Width - size.Width;
else
x = Location.X;
if (Location.Y + Size.Height > Screen.PrimaryScreen.Bounds.Height)
y = Screen.PrimaryScreen.Bounds.Height - size.Height;
else
y = Location.Y;
}
else
{
size = RestoreBounds.Size;
x = (Screen.PrimaryScreen.Bounds.Width - size.Width)/2;
y = (Screen.PrimaryScreen.Bounds.Height - size.Height)/2;
}
Properties.Settings.Position.AsPoint = new Point(x, y); // Property setting is type of Point
Properties.Settings.Size.AsSize = size; // Property setting is type of Size
Properties.Settings.SplitterDistance.Value = splitContainer1.SplitterDistance; // Property setting is type of int
Properties.Settings.IsMaximized = WindowState == FormWindowState.Maximized; // Property setting is type of bool
Properties.Settings.DropDownSelection = DropDown1.SelectedValue;
Properties.Settings.Save();
A hack you can use Settings to store that information. All you have to do is bind the desired property (ex. form.Size and form.Location) to a specific setting and it get saved and updated automatically.
Update: Solved, with code
I got it working, see my answer below for the code...
Original Post
As Tundey pointed out in his answer to my last question, you can bind nearly everything about a windows forms control to ApplicationSettings pretty effortlessly. So is there really no way to do this with form Size? This tutorial says you need to handle Size explicitly so you can save RestoreBounds instead of size if the window is maximized or minimized. However, I hoped I could just use a property like:
public Size RestoreSize
{
get
{
if (this.WindowState == FormWindowState.Normal)
{
return this.Size;
}
else
{
return this.RestoreBounds.Size;
}
}
set
{
...
}
}
But I can't see a way to bind this in the designer (Size is notably missing from the PropertyBinding list).
I finally came up with a Form subclass that solves this, once and for all. To use it:
Inherit from RestorableForm instead of Form.
Add a binding in (ApplicationSettings) -> (PropertyBinding) to WindowRestoreState.
Call Properties.Settings.Default.Save() when the window is about to close.
Now window position and state will be remembered between sessions. Following the suggestions from other posters below, I included a function ConstrainToScreen that makes sure the window fits nicely on the available displays when restoring itself.
Code
// Consider this code public domain. If you want, you can even tell
// your boss, attractive women, or the other guy in your cube that
// you wrote it. Enjoy!
using System;
using System.Windows.Forms;
using System.ComponentModel;
using System.Drawing;
namespace Utilities
{
public class RestorableForm : Form, INotifyPropertyChanged
{
// We invoke this event when the binding needs to be updated.
public event PropertyChangedEventHandler PropertyChanged;
// This stores the last window position and state
private WindowRestoreStateInfo windowRestoreState;
// Now we define the property that we will bind to our settings.
[Browsable(false)] // Don't show it in the Properties list
[SettingsBindable(true)] // But do enable binding to settings
public WindowRestoreStateInfo WindowRestoreState
{
get { return windowRestoreState; }
set
{
windowRestoreState = value;
if (PropertyChanged != null)
{
// If anybody's listening, let them know the
// binding needs to be updated:
PropertyChanged(this,
new PropertyChangedEventArgs("WindowRestoreState"));
}
}
}
protected override void OnClosing(CancelEventArgs e)
{
WindowRestoreState = new WindowRestoreStateInfo();
WindowRestoreState.Bounds
= WindowState == FormWindowState.Normal ?
Bounds : RestoreBounds;
WindowRestoreState.WindowState = WindowState;
base.OnClosing(e);
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
if (WindowRestoreState != null)
{
Bounds = ConstrainToScreen(WindowRestoreState.Bounds);
WindowState = WindowRestoreState.WindowState;
}
}
// This helper class stores both position and state.
// That way, we only have to set one binding.
public class WindowRestoreStateInfo
{
Rectangle bounds;
public Rectangle Bounds
{
get { return bounds; }
set { bounds = value; }
}
FormWindowState windowState;
public FormWindowState WindowState
{
get { return windowState; }
set { windowState = value; }
}
}
private Rectangle ConstrainToScreen(Rectangle bounds)
{
Screen screen = Screen.FromRectangle(WindowRestoreState.Bounds);
Rectangle workingArea = screen.WorkingArea;
int width = Math.Min(bounds.Width, workingArea.Width);
int height = Math.Min(bounds.Height, workingArea.Height);
// mmm....minimax
int left = Math.Min(workingArea.Right - width,
Math.Max(bounds.Left, workingArea.Left));
int top = Math.Min(workingArea.Bottom - height,
Math.Max(bounds.Top, workingArea.Top));
return new Rectangle(left, top, width, height);
}
}
}
Settings Bindings References
SettingsBindableAttribute
INotifyPropertyChanged
The reason why the Form.Size property is not available in the settings binding UI is because this property is marked DesignerSerializationVisibility.Hidden. This means that the designer doesn't know how to serialise it, let alone generate a data binding for it. Instead the Form.ClientSize property is the one that gets serialised.
If you try and get clever by binding Location and ClientSize, you'll see another problem. When you try to resize your form from the left or top edge, you'll see weird behaviour. This is apparently related to the way that two-way data binding works in the context of property sets that mutually affect each other. Both Location and ClientSize eventually call into a common method, SetBoundsCore().
Also, data binding to properties like Location and Size is just not efficient. Each time the user moves or resizes the form, Windows sends hundreds of messages to the form, causing the data binding logic to do a lot of processing, when all you really want is to store the last position and size before the form is closed.
This is a very simplified version of what I do:
private void MyForm_FormClosing(object sender, FormClosingEventArgs e)
{
Properties.Settings.Default.MyState = this.WindowState;
if (this.WindowState == FormWindowState.Normal)
{
Properties.Settings.Default.MySize = this.Size;
Properties.Settings.Default.MyLoc = this.Location;
}
else
{
Properties.Settings.Default.MySize = this.RestoreBounds.Size;
Properties.Settings.Default.MyLoc = this.RestoreBounds.Location;
}
Properties.Settings.Default.Save();
}
private void MyForm_Load(object sender, EventArgs e)
{
this.Size = Properties.Settings.Default.MySize;
this.Location = Properties.Settings.Default.MyLoc;
this.WindowState = Properties.Settings.Default.MyState;
}
Why is this a very simplified version? Because doing this properly is a lot trickier than it looks :-)
One of the reason I imagine size binding is not allowed is because the screen may change between sessions.
Loading the size back when the resolution has reduced could result in the title bar being beyond the limits of the screen.
You also need to be wary of multiple monitor setups, where monitors may no longer be available when you app next runs.
Well I have had a quick play with this and you are correct, while there is no way to directly bind the size of the form to AppSettings, you can add your own values and change the size on load.
I would perhaps recommend that if this is a common feature, you subclass Form and make it automatically prob the App.Config for the forms size settings.
(Or you could roll your own file.. Get it to query an Xml file "formname.settings.xml" or something? - thinking out loud!)..
Heres what I had (very rough, no error checking etc).
App.Config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key ="FormHeight" value="500" />
<add key ="FormWidth" value="200"/>
</appSettings>
</configuration>
Form Code
private void Form1_Load(object sender, EventArgs e)
{
string height = ConfigurationManager.AppSettings["FormHeight"];
int h = int.Parse(height);
string width = ConfigurationManager.AppSettings["FormWidth"];
int w = int.Parse(width);
this.Size = new Size(h, w);
}
I agree with Rob Cooper's answer. But I think Martin makes a very good point. Nothing like having users open your application and the app is off-screen!
So in reality, you'll want to combine both answers and bear in mind the current screen dimensions before setting your form's size.