C# saving colours of many things - c#

I have managed to get a colour-picker on the go which works, but I want to be able to save the colours the user wants. I have made links to the colordialog for labels/comboboxes/other controls as seperate items which works, but I would like to know how to save them (maybe to a txt file or something) as when the program restarts, it reverts back to black and white.
Thing is, it's not just 1 or 2 colour changes, I have got 35 labels, 7 comboboxes, 4 radiobuttons, 5 tabpages, 22 textboxes (text & background of each), 10 buttons (text and background of each) and A LOT of numericupdowns (text and background of each) - I have just spent the last hour sorting each one with the colour-picker...I don't want to have to do something to each thing in turn if I can help it...is there a way, or am I going to have to go through them all :)

I would follow the process of saving the colors on form closing, loading the colors on the form load, then applying the default colors after they have loaded.
I store the settings into a global variable inside of the form so I can reference them by name.
Settings Variable:
public Dictionary<string, FieldColor> colorSettings = new Dictionary<string, FieldColor>();
I use this following structure because you currently have two colors, but you could add more properties should the need arise.
Data Structure:
public struct FieldColor
{
public Color BackColor;
public Color ForeColor;
public FieldColor(Color backColor, Color foreColor)
{
this.BackColor = backColor;
this.ForeColor = foreColor;
}
}
The save method iterates through each control, then iterates through it's children controls, and if the color isn't the form's default color it saves the line. This saves it as a csv.
Save:
public void SaveFieldColors(Control parent, StreamWriter colorWriter)
{
foreach (Control currentControl in parent.Controls)
{
if (currentControl.ForeColor != DefaultForeColor || currentControl.BackColor != DefaultBackColor)
{
//Save the file in your format
colorWriter.WriteLine(currentControl.Name + "," + currentControl.BackColor.ToArgb() + "," + currentControl.ForeColor.ToArgb());
}
ApplyDefaultColorToControls(currentControl);
}
}
To load the settings I just break each line into a entry into the colorSettings dictionary.
Load:
public void LoadFieldColors(StreamReader colorReader)
{
while (colorReader.EndOfStream == false)
{
//Read the file from your format
string line = colorReader.ReadLine();
string[] fieldData = line.Split(',');
colorSettings.Add(fieldData[0], new FieldColor(Color.FromArgb(int.Parse(fieldData[1])), Color.FromArgb(int.Parse(fieldData[2]))));
}
}
Here I iterate through each control and their children to apply the formatting if there's an entry for that control's name. If there isn't one I just set it to the default.
Apply the Formatting:
public void ApplyDefaultColorToControls(Control parent)
{
foreach (Control currentControl in parent.Controls)
{
if (colorSettings.ContainsKey(currentControl.Name) == true)
{
//Set the controls settings equal to their color in your stored dictionary
currentControl.BackColor = colorSettings[currentControl.Name].BackColor;
currentControl.ForeColor = colorSettings[currentControl.Name].ForeColor;
}
else
{
//Set the control settings equal to default settings
currentControl.BackColor = DefaultBackColor;
currentControl.ForeColor = DefaultForeColor;
}
ApplyDefaultColorToControls(currentControl);
}
}
Example Calls from the form events:
private void Form1_Load(object sender, EventArgs e)
{
using (StreamReader settingsReader = new StreamReader(#"C:\path\to\file\filename.ending"))
{
LoadFieldColors(settingsReader);
}
ApplyDefaultColorToControls(this);
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
using (StreamWriter settingsWriter = new StreamWriter(#"C:\path\to\file\filename.ending"))
{
SaveFieldColors(this, settingsWriter);
}
}
}

There are many ways to store data in c#, starting with a simple text file and ending in databases... this is too broad. However you choose to save the data, I suggest to start with a Dictonary<string, Touple<color, color>> where the string is the control name, one is the backColor and the other is the foreColor and serialize it into a file (xml or json, doesn't matter all that much).
When you load your form, use a recursive function on
this.Controls to change the colors for each control.

You can try this if it is for WinForms project.
Create class for your settings :
[Serializable]
sealed class Settings
{
internal Dictionary<string, Color> ForeColors { get; set; }
internal Dictionary<string, Color> BackColors { get; set; }
internal Settings()
{
ForeColors = new Dictionary<string, Color>();
BackColors = new Dictionary<string, Color>();
}
internal void Save(string fileName = #"settings.ini")
{
using (FileStream stream = File.Create(Directory.GetCurrentDirectory() + "\\" + fileName))
{
BinaryFormatter serializer = new BinaryFormatter();
serializer.Serialize(stream, this);
}
}
internal static Settings Load(string fileName = #"settings.ini")
{
if (!File.Exists(Directory.GetCurrentDirectory() + "\\" + fileName)) return null;
using (FileStream stream = File.OpenRead(Directory.GetCurrentDirectory() + "\\" + fileName))
{
BinaryFormatter serializer = new BinaryFormatter();
return serializer.Deserialize(stream) as Settings;
}
}
}
Now in your windows form subscribe to Form_Load Form_Closing events and add handlers:
private void Form1_Load(object sender, EventArgs e)
{
Settings s = Settings.Load();
if (s == null) return;
foreach (Control item in this.Controls)
{
item.ForeColor = s.ForeColors[item.Name];
item.BackColor = s.BackColors[item.Name];
}
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
Settings s = new Settings();
foreach (Control item in this.Controls)
{
s.ForeColors.Add(item.Name, item.ForeColor);
s.BackColors.Add(item.Name, item.BackColor);
}
s.Save();
}

Related

Cell selection hides behind image DataGridViewImageCell

I'm using Winform DataGridView to display images. But when image fills cell, I don't see blue selection or in very low quantity. Please see:
When an cell is selected, I expect make cell whole transparent blue, not just sides or sides which isn't occupied by image. like:
Currently I tried coloring blue myself in paint event but it updates too frequently which hangs software.
I also modify image to look bluish in selection changed event, but again it slows down software.
Is there fix to this ? any workaround or something ? without compromising performance ?
EDIT: This is source code on how I display images on datagridview:
int colms = 4; // total no. of columns in our datagridview
//this create 4 image columns in datagridview
for (int c = 0; c < colms; c++)
{
var imgColm = new DataGridViewImageColumn();
imgColm.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
imgColm.ImageLayout = DataGridViewImageCellLayout.Zoom;
grid.Columns.Add(imgColm);
}
int colm = 0;
int row = 0;
//this get all images and display on datagridview
foreach (var img in Directory.GetFiles(#"C:\Users\Administrator\Desktop\images"))
{
if (colm >= colms)
{
row++;
colm = 0;
grid.Rows.Add();
}
((DataGridViewImageCell)grid.Rows[row].Cells[colm]).Value = Thumb.GetThumbnail(img, ThumbSize.LowRes);
colm++;
}
Currently cell painting I use just a workaround, that draws border on selected cell. But its slow when data is large and secondly draws on unselected cell as well.
Here's two examples to test yourself regarding the performance and the fill style of the selected cells. Since your code snippet does not show in what context the code is called, especially creating the image columns part, and to avoid repeating unnecessary routines, use the grid designer to add 4 columns of type DataGridViewImageColumn and set the auto size and layout properties from there.
Normal Mode
In the Form's ctor, use Reflection to enable the grid's DoubleBuffered property to reduce the flicker. The emptyImage bitmap is the null value of the empty cells.
public partial class SomeForm : Form
{
private Bitmap emptyImage;
public SomeForm()
{
dgv.GetType()
.GetProperty("DoubleBuffered", BindingFlags.Instance | BindingFlags.NonPublic)
.SetValue(dgv, true);
emptyImage = new Bitmap(1, 1);
foreach (var col in dgv.Columns.OfType<DataGridViewImageColumn>())
col.DefaultCellStyle.NullValue = emptyImage;
}
Override the OnLoad method to populate the grid or to call a method for that.
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
var imgFolder = #"Your-Image-Folder-Path";
LoadImages(imgFolder);
}
protected override void OnFormClosed(FormClosedEventArgs e)
{
base.OnFormClosed(e);
emptyImage.Dispose();
}
private void LoadImages(string path)
{
string[] allFiles = Directory.GetFiles(path);
for (int i = 0; i < allFiles.Length; i += 4)
{
var files = allFiles.Skip(i).Take(4);
dgv.Rows.Add(
files.Select(f =>
{
using (var img = Image.FromFile(f, true))
return Thumb.GetThumbnail(img, ThumbSize.LowRes);
}).ToArray());
}
}
Note, your Thumb.GetThumbnail method returns a new image so you need to dispose of the original image.
Implement the CellPainting event to draw everything except the DataGridViewPaintParts.SelectionBackground and fill the selected cells with a semi-transparent color.
private void dgv_CellPainting(object sender, DataGridViewCellPaintingEventArgs e)
{
e.Paint(e.ClipBounds, e.PaintParts & ~DataGridViewPaintParts.SelectionBackground);
if (e.RowIndex >= 0 &&
e.Value != null && e.Value != emptyImage &&
(e.State & DataGridViewElementStates.Selected) > 0)
{
using (var br = new SolidBrush(Color.FromArgb(100, SystemColors.Highlight)))
e.Graphics.FillRectangle(br, e.CellBounds);
}
e.Handled = true;
}
}
Virtual Mode
You need here to have data store to cache only the images you need to display. The images of each cell of the visible rows. For that, The Cache class is
created to manage the relevant functionalities including:
Calculating the total number of rows required to display 4 images per visible row. So the SetMaxRows method should be called when the grid is first created and resized to recalculate the visible rows.
Loading, creating, and caching the current visible rows images in a Dictionary<int, Image> where the keys are the cell numbers.
Passing the requested images when the CellValueNeeded event is raised.
public partial class SomeForm : Form
{
private readonly Cache cache;
public SomeForm()
{
dgv.GetType()
.GetProperty("DoubleBuffered", BindingFlags.Instance | BindingFlags.NonPublic)
.SetValue(dgv, true);
dgv.VirtualMode = true;
var imgFolder = #"Your-Image-Folder-Path";
cache = new Cache(imgFolder, dgv.ColumnCount);
dgv.RowCount = cache.GetRowCount();
SetMaxRows();
}
protected override void OnFormClosed(FormClosedEventArgs e)
{
base.OnFormClosed(e);
cache.Dispose();
}
private void dgv_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e) =>
e.Value = cache.GetImage(e.RowIndex, e.ColumnIndex);
private void dgv_Resize(object sender, EventArgs e) => SetMaxRows();
// Change dgv.RowTemplate.Height as needed...
private void SetMaxRows() =>
cache.MaxRows = (int)Math
.Ceiling((double)dgv.ClientRectangle.Height / dgv.RowTemplate.Height);
private class Cache : IDisposable
{
private readonly Dictionary<int, Image> dict;
private readonly Bitmap nullImage;
private int currentRowIndex = -1;
private Cache()
{
dict = new Dictionary<int, Image>();
nullImage = new Bitmap(1, 1);
}
public Cache(string path, int columnCount) : this()
{
ImageFolder = path;
ColumnCount = columnCount;
}
public string ImageFolder { get; set; }
public int ColumnCount { get; set; }
public int MaxRows { get; set; }
public Bitmap NullImage => nullImage;
public Image GetImage(int rowIndex, int columnIndex)
{
var ri = rowIndex - (rowIndex % MaxRows);
if (ri != currentRowIndex)
{
foreach (var img in dict.Values) img?.Dispose();
currentRowIndex = ri;
dict.Clear();
}
var i = (rowIndex * ColumnCount) + columnIndex;
Image res = nullImage;
if (!dict.ContainsKey(i))
{
var file = Directory.EnumerateFiles(ImageFolder)
.Skip(i).FirstOrDefault();
if (file != null)
{
using (var img = Image.FromFile(file, true))
dict[i] = res = Thumb.GetThumbnail(img, ThumbSize.LowRes);
}
}
else
{
res = dict[i];
}
return res;
}
public int GetRowCount()
{
var count = Directory.EnumerateFiles(ImageFolder).Count();
return (int)Math.Ceiling((double)count / ColumnCount);
}
public void Dispose()
{
foreach (var img in dict.Values) img?.Dispose();
nullImage.Dispose();
}
}
Finally, the CellPainting event remains almost the same except that you get the null image from the cache instance.
private void dgv_CellPainting(object sender, DataGridViewCellPaintingEventArgs e)
{
e.Paint(e.ClipBounds, e.PaintParts & ~DataGridViewPaintParts.SelectionBackground);
if (e.RowIndex >= 0 &&
e.Value != null && e.Value != cache.NullImage &&
(e.State & DataGridViewElementStates.Selected) > 0)
{
using (var br = new SolidBrush(Color.FromArgb(100, SystemColors.Highlight)))
e.Graphics.FillRectangle(br, e.CellBounds);
}
e.Handled = true;
}
}

moving images in visual studio c#

My program consists of a grid with 8 images (one free space). At the moment when you click on one image and then another they swap, but I only want the image to move one space at a time and only if there is a clear box. (like those scramble games)
I have this swap method, but how can I change it to fit what I want?
string click1Name="";
string click1Loc="";
string click2Name="";
string click2Loc="";
private void swap()
{
var objName=(Image)this.FindName(click1Loc);
objName.Source = new BitmapImage(new Uri("pack://application:,,,/Pic1/"+click2Name));
objName = (Image)this.FindName(click2Loc);
objName.Source = new BitmapImage(new Uri("pack://application:,,,/Pic1/" + click1Name));
}
GridPic9 is the free space (Pic9.jpg)
Not sure of what your problem is. I'm guessing when you click over one Image, you load its name and location into the corresponding global variables (click(x)Name and click(x)Loc); and then, when you click over the second Image, you load again name and location, and call Swap afterwards. Is that it?
In order to manage the swapping constrains that you stated, I would create a matrix of objects. Each object would contain info about what is shown in each Image control. The following is a naive way to do it, but it can give you an idea.
class ImageInfo
{
public string Name;
Image AssociatedControl;
public ImageInfo(string name, string loc)
{
Name=name;
AssociatedControl=(Image)this.FindName(loc);
DisplayToControl();
}
void DisplayToControl()
{
if(Name!=null)
AssociatedControl.Source = new BitmapImage(new Uri("pack://application:,,,/Pic1/"+Name));
else
AssociatedControl.Source =null;
}
public void SetImage(string name)
{
Name=name;
DisplayToControl()
}
}
ImageInfo[][] myImagesLayout = new ImageInfo[3][3];
Tuple<int,int> PreviousClickedImageCoords = null;
void myWindow_Load()
{
//Initialize the 8 images in their initial positions
myImagesLayout [0][0] = new myImagesLayout(name1,location1);
//[...]
myImagesLayout [2][1] = new myImagesLayout(name8,location8);
myImagesLayout [2][2] = new myImagesLayout(null,location9);
}
void Image1_Clicked(object sender, RoutedEventArgs e)
{
Clicked(0,0);
}
//[...]
void Image9_Clicked(object sender, RoutedEventArgs e)
{
Clicked(2,2);
}
void Clicked(int row, int column)
{
if(PreviousClickedImageCoords == null )
{
if(myImagesLayout[row][column].Name!=null)
PreviousClickedImageCoords = new Tuple<int,int>(row,column);
}
else
{
if(myImagesLayout[row][column].Name!=null)
return; //only to empty images
int prevRow=PreviousClickedImageCoords.Item1;
int prevCol=PreviousClickedImageCoords.Item2;
int step = Math.Abs(Row-prevRow) + Math.Abs(Col-prevCol);
if(step!=1)
return; //only adjacent cells. No diagonals
ImageInfo PreviousClickedImage = myImagesLayout[prevRow][prevCol];
myImagesLayout[row][column].SetImage(PreviousClickedImage.Name);
PreviousClickedImage.SetImage(null);
PreviousClickedImage=null;
}
}

Remove label created on Run-time CF C#

I create a label "label1" dynamically in a method. Then when I click a button I want to remove that label created but if I write Controls.Remove(label1) it says that the control doesn't exist in the context.
How could I do to achieve this?
EDIT: Following Jon suggestion I implemented the foreach loop but it doesn't do anything. This is my code, the panel which I use is created by design:
void GenerateControls() {
Label labelOne = new Label();
Button btnContinue = new Button();
panel.SuspendLayout();
SuspendLayout();
//btnContinue
btnContinue.BackColor = System.Drawing.Color.Black;
btnContinue.ForeColor = System.Drawing.SystemColors.Menu;
btnContinue.Location = new System.Drawing.Point(145, 272);
btnContinue.Name = "btnContinue";
btnContinue.Size = new System.Drawing.Size(95, 28);
btnContinue.TabIndex = 13;
btnContinue.Text = "Continue";
btnContinue.Visible = true;
Controls.Add(btnContinue);
btnContinue.Click += new System.EventHandler(btnContinue_Click);
//labelOne
labelOne.Location = new Point(0,65);
labelOne.Size = new System.Drawing.Size(100,20);
labelOne.Text = "labelOne";
labelOne.Name = "labelOne";
labelOne.Visible = true;
labelOne.TextChanged += new System.EventHandler(this.lbl_TextChanged);
labelOne.BackColor = System.Drawing.Color.PaleGreen;
Controls.Add(labelOne);
//panel
panel.Controls.Add(labelOne);
panel.Visible = true;
panel.Location = new Point(0,0);
panel.Size = new Size(240, 320);
//
Controls.Add(panel);
panel.ResumeLayout();
ResumeLayout();
}
And then in when I click on btnContinue:
private void btnContinuar_Click(object sender, EventArgs e) {
foreach (Control control in panel.Controls) {
if (control.Name == "labelOne"){
panel.Controls.Remove(control);
break;
}
}
}
I debug it and in the panel.Control it continues as if it were empty panel.
Thanks for your help!
I suspect it says the variable doesn't exist in that context. You'll have to find the label by its text, or knowing something else about it. For example, when you create it you could set the Name property and find it by that when you want to remove it:
panel.Controls.RemoveByKey("YourLabelName");
EDIT: As noted in the comments, RemoveByKey doesn't exist in the compact framework. So you'd either have to remember the reference yourself (in which case you don't need the name) or use something like:
foreach (Control control in panel.Controls)
{
if (control.Name == "YourLabelName")
{
panel.Controls.Remove(control);
break;
}
}
EDIT2: And to make it even more "generic" and desktop compatible, you could keep the RemoveByKey call and add this to your app:
public static class FormExtensions
{
public static void RemoveByKey(this Control.ControlCollection collection,
string key)
{
if(!RemoveChildByName(collection, key))
{
throw new ArgumentException("Key not found");
}
}
private static bool RemoveChildByName(
this Control.ControlCollection collection,
string name)
{
foreach (Control child in collection)
{
if (child.Name == name)
{
collection.Remove(child);
return true;
}
// Nothing found at this level: recurse down to children.
if (RemoveChildByName(child.Controls, name))
{
return true;
}
}
return false;
}
}
After 20 edits to the OP question, and Jon's answer with no resemblance to the original problem, you are left with one small glitch.
Your Not adding labelOne to the panel you are adding it to the Form.
Change
Controls.Add(labelOne);
to
panel.Controls.Add(labelOne);
Then everything should work

How to print next line on Listbox

I have been tearing my hair out with this problem, my ToString is repeating straight after each other rather than dropping to the next line.
as shown on this image, the second Event name should be on the line below.
//ToString method in Event Class
public override string ToString()
{
return "\nEvent name : " + m_evName + "\n Date :" + m_evDate + "\n";
}
//print method in class
public String PrintEvents()
{
StringBuilder retev = new StringBuilder("");
foreach (Event e in m_events)
{
retev.Append(e.ToString() + "\n");
}
return retev.ToString();
}
//Foreach that displays the text
private void cboListEv_SelectedIndexChanged(object sender, EventArgs e)
{
String SelectedVenue = cboListEv.Text;
List<Venue> found = plan.selectVen(SelectedVenue);
lstEvents.Items.Clear();
foreach (Venue v in found)
{
lstEvents.Items.Add(v.PrintEvents());
}
}
It's not possible to print multiple text lines per ListBox item with the standard ListBox. Try using a TextBox instead, with Multiline = true, and in read-only mode. It will achieve a similar effect.
Beyond that, you'll need to draw up your own custom ListBox control with customized item templates that support multiple lines of data.
ListBox items don't support multiline strings. However, you can make the ListBox's DrawMode OwnerDrawVariable, then hook up something like the following to its MeasureItem and DrawItem events:
internal int CountOccurrences(string haystack, string needle) {
int n = 0, pos = 0;
while((pos = haystack.IndexOf(needle, pos)) != -1) {
n++;
pos += needle.Length;
}
return n;
}
void ListBox1MeasureItem(object sender, MeasureItemEventArgs e)
{
e.ItemHeight = (int)((CountOccurrences(((ListBox)sender).Items[e.Index].ToString(), "\n") + 1) * ((ListBox)sender).Font.GetHeight() + 2);
}
void ListBox1DrawItem(object sender, DrawItemEventArgs e)
{
string text = ((ListBox)sender).Items[e.Index].ToString();
e.DrawBackground();
using(Brush b = new SolidBrush(e.ForeColor)) e.Graphics.DrawString(text, e.Font, b, new RectangleF(e.Bounds.Left, e.Bounds.Top, e.Bounds.Width, e.Bounds.Height));
e.DrawFocusRectangle();
}
-- will end up something like this:
If you provide the PrintEvents() method with the item collection of the ListBox you can have it add an item for every found event. Something like this:
//print method in class
public String PrintEvents(ObjectCollection items)
{
foreach (Event e in m_events)
items.Add(e.ToString());
}
//Foreach that displays the text
private void cboListEv_SelectedIndexChanged(object sender, EventArgs e)
{
String SelectedVenue = cboListEv.Text;
List<Venue> found = plan.selectVen(SelectedVenue);
lstEvents.Items.Clear();
foreach (Venue v in found)
v.PrintEvents(lstEvents.Items);
}
Add an Events property to the Venue class if not already there
public List<Event> Events
{
get { return m_events; }
}
Then add the items like this
foreach (Venue v in found) {
foreach (Event e in v.Events) {
lstEvents.Items.Add(e);
}
}

.NET / Windows Forms: remember windows size and location

I have a Windows Forms application with a normal window. Now when I close the application and restart it, I want that the main window appears at the same location on my screen with the same size of the moment when it was closed.
Is there an easy way in Windows Forms to remember the screen location and window size (and if possible the window state) or does everything have to be done by hand?
If you add this code to your FormClosing event handler:
if (WindowState == FormWindowState.Maximized)
{
Properties.Settings.Default.Location = RestoreBounds.Location;
Properties.Settings.Default.Size = RestoreBounds.Size;
Properties.Settings.Default.Maximised = true;
Properties.Settings.Default.Minimised = false;
}
else if (WindowState == FormWindowState.Normal)
{
Properties.Settings.Default.Location = Location;
Properties.Settings.Default.Size = Size;
Properties.Settings.Default.Maximised = false;
Properties.Settings.Default.Minimised = false;
}
else
{
Properties.Settings.Default.Location = RestoreBounds.Location;
Properties.Settings.Default.Size = RestoreBounds.Size;
Properties.Settings.Default.Maximised = false;
Properties.Settings.Default.Minimised = true;
}
Properties.Settings.Default.Save();
It will save the current state.
Then add this code to your form's OnLoad handler:
if (Properties.Settings.Default.Maximised)
{
Location = Properties.Settings.Default.Location;
WindowState = FormWindowState.Maximized;
Size = Properties.Settings.Default.Size;
}
else if (Properties.Settings.Default.Minimised)
{
Location = Properties.Settings.Default.Location;
WindowState = FormWindowState.Minimized;
Size = Properties.Settings.Default.Size;
}
else
{
Location = Properties.Settings.Default.Location;
Size = Properties.Settings.Default.Size;
}
It will restore the last state.
It even remembers which monitor in a multi monitor set up the application was maximised to.
You'll need to save the window location and size in your application settings. Here's a good C# article to show you how.
EDIT
You can save pretty much anything you want in the application settings. In the Type column of the settings grid you can browse to any .NET type. WindowState is in System.Windows.Forms and is listed as FormWindowState. There's also a property for FormStartPosition.
I tried a few different methods; this is what ended up working for me.
(In this case - on first launch - the defaults haven't been persisted yet, so the form will use the values set in the designer)
Add the settings to the project (manually - don't rely on visual studio):
Add the following code to your form:
private void Form1_Load(object sender, EventArgs e)
{
this.RestoreWindowPosition();
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
this.SaveWindowPosition();
}
private void RestoreWindowPosition()
{
if (Settings.Default.HasSetDefaults)
{
this.WindowState = Settings.Default.WindowState;
this.Location = Settings.Default.Location;
this.Size = Settings.Default.Size;
}
}
private void SaveWindowPosition()
{
Settings.Default.WindowState = this.WindowState;
if (this.WindowState == FormWindowState.Normal)
{
Settings.Default.Location = this.Location;
Settings.Default.Size = this.Size;
}
else
{
Settings.Default.Location = this.RestoreBounds.Location;
Settings.Default.Size = this.RestoreBounds.Size;
}
Settings.Default.HasSetDefaults = true;
Settings.Default.Save();
}
Previous solutions didn't work for me. After playing a while I ended up with following code which:
preserves maximised and normal state
replaces minimised state with default position
in case of screen size changes (detached monitor, remote connection,...) it will not get user into frustrating state with application open outside of screen.
private void MyForm_Load(object sender, EventArgs e)
{
if (Properties.Settings.Default.IsMaximized)
WindowState = FormWindowState.Maximized;
else if (Screen.AllScreens.Any(screen => screen.WorkingArea.IntersectsWith(Properties.Settings.Default.WindowPosition)))
{
StartPosition = FormStartPosition.Manual;
DesktopBounds = Properties.Settings.Default.WindowPosition;
WindowState = FormWindowState.Normal;
}
}
private void MyForm_FormClosing(object sender, FormClosingEventArgs e)
{
Properties.Settings.Default.IsMaximized = WindowState == FormWindowState.Maximized;
Properties.Settings.Default.WindowPosition = DesktopBounds;
Properties.Settings.Default.Save();
}
user settings:
<userSettings>
<WindowsFormsApplication2.Properties.Settings>
<setting name="WindowPosition" serializeAs="String">
<value>0, 0, -1, -1</value>
</setting>
<setting name="IsMaximized" serializeAs="String">
<value>False</value>
</setting>
</WindowsFormsApplication2.Properties.Settings>
</userSettings>
Note: the WindowsPosition is intentionally wrong, so during first launch application will use default location.
Note that IntersectsWith expects a Rectangle, not a Point. So unlike other answers, this answer is saving the DesktopBounds, not Location, into Properties.Settings.Default.WindowPosition
If you use the fabulous open source library - Jot, you can forget about the tedious .settings files and just do this:
public MainWindow()
{
InitializeComponent();
_stateTracker.Configure(this)
.IdentifyAs("MyMainWindow")
.AddProperties(nameof(Height), nameof(Width), nameof(Left), nameof(Top), nameof(WindowState))
.RegisterPersistTrigger(nameof(Closed))
.Apply();
}
There's a Nuget package as well, and you can configure pretty much everything about how/when/where data is stored.
Disclaimer: I'm the author, but the library is completely open source (under MIT license).
Matt - to save the WindowState as a user setting, in the Settings Dialog, in the "Type" dropdown, scroll to the bottom and select "Browse".
In the "Select a Type" dialog, expand System.Windows.Forms and you can choose "FormWindowState" as the type.
(sorry, I don't see a button that allows me to comment on the comment...)
If you have more than 1 form you can use something like this...
Add this part all form load void
var AbbA = Program.LoadFormLocationAndSize(this);
this.Location = new Point(AbbA[0], AbbA[1]);
this.Size = new Size(AbbA[2], AbbA[3]);
this.FormClosing += new FormClosingEventHandler(Program.SaveFormLocationAndSize);
Save form location and size to app.config xml
public static void SaveFormLocationAndSize(object sender, FormClosingEventArgs e)
{
Form xForm = sender as Form;
Configuration config = ConfigurationManager.OpenExeConfiguration(Application.ExecutablePath);
if (ConfigurationManager.AppSettings.AllKeys.Contains(xForm.Name))
config.AppSettings.Settings[xForm.Name].Value = String.Format("{0};{1};{2};{3}", xForm.Location.X, xForm.Location.Y, xForm.Size.Width, xForm.Size.Height);
else
config.AppSettings.Settings.Add(xForm.Name, String.Format("{0};{1};{2};{3}", xForm.Location.X, xForm.Location.Y, xForm.Size.Width, xForm.Size.Height));
config.Save(ConfigurationSaveMode.Full);
}
Load form location and size from app.config xml
public static int[] LoadFormLocationAndSize(Form xForm)
{
int[] LocationAndSize = new int[] { xForm.Location.X, xForm.Location.Y, xForm.Size.Width, xForm.Size.Height };
//---//
try
{
Configuration config = ConfigurationManager.OpenExeConfiguration(Application.ExecutablePath);
var AbbA = config.AppSettings.Settings[xForm.Name].Value.Split(';');
//---//
LocationAndSize[0] = Convert.ToInt32(AbbA.GetValue(0));
LocationAndSize[1] = Convert.ToInt32(AbbA.GetValue(1));
LocationAndSize[2] = Convert.ToInt32(AbbA.GetValue(2));
LocationAndSize[3] = Convert.ToInt32(AbbA.GetValue(3));
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
//---//
return LocationAndSize;
}
You'll have to manually save the information somewhere. I'd suggest doing so as application settings, storing them in user specific isolated storage.
Once you load up, read the settings then resize/move your form.
My answer is adapted from ChrisF♦'s answer, but I've fixed one thing I didn't like - if the window is minimized at the time of closing, it would appear minimized on next start.
My code handles that case correctly by remembering whether the window was maximized or normal at the time of its minimization, and setting the persistent state accordingly.
Unfortunately, Winforms doesn't expose that information directly, so I needed to override WndProc and store it myself. See Check if currently minimized window was in maximized or normal state at the time of minimization
partial class Form1 : Form
{
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_SYSCOMMAND)
{
int wparam = m.WParam.ToInt32() & 0xfff0;
if (wparam == SC_MAXIMIZE)
LastWindowState = FormWindowState.Maximized;
else if (wparam == SC_RESTORE)
LastWindowState = FormWindowState.Normal;
}
base.WndProc(ref m);
}
private const int WM_SYSCOMMAND = 0x0112;
private const int SC_MAXIMIZE = 0xf030;
private const int SC_RESTORE = 0xf120;
private FormWindowState LastWindowState;
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (WindowState == FormWindowState.Normal)
{
Properties.Settings.Default.WindowLocation = Location;
Properties.Settings.Default.WindowSize = Size;
}
else
{
Properties.Settings.Default.WindowLocation = RestoreBounds.Location;
Properties.Settings.Default.WindowSize = RestoreBounds.Size;
}
if (WindowState == FormWindowState.Minimized)
{
Properties.Settings.Default.WindowState = LastWindowState;
}
else
{
Properties.Settings.Default.WindowState = WindowState;
}
Properties.Settings.Default.Save();
}
private void Form1_Load(object sender, EventArgs e)
{
if (Properties.Settings.Default.WindowSize != new Size(0, 0))
{
Location = Properties.Settings.Default.WindowLocation;
Size = Properties.Settings.Default.WindowSize;
WindowState = Properties.Settings.Default.WindowState;
}
}
You could also save it in your (let's say) config.xml when you close the form:
private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
XmlDocument docConfigPath = new XmlDocument();
docConfigPath.Load(XML_Config_Path);
WriteNode(new string[] { "config", "Size", "Top", Top.ToString() }, docConfigPath);
WriteNode(new string[] { "config", "Size", "Left", Left.ToString() }, docConfigPath);
WriteNode(new string[] { "config", "Size", "Height", Height.ToString() }, docConfigPath);
WriteNode(new string[] { "config", "Size", "Width", Width.ToString() }, docConfigPath);
docConfigPath.Save(XML_Config_Path);
}
public static XmlNode WriteNode(string[] sNode, XmlDocument docConfigPath)
{
int cnt = sNode.Length;
int iNode = 0;
string sNodeNameLast = "/" + sNode[0];
string sNodeName = "";
XmlNode[] xN = new XmlNode[cnt];
for (iNode = 1; iNode < cnt - 1; iNode++)
{
sNodeName = "/" + sNode[iNode];
xN[iNode] = docConfigPath.SelectSingleNode(sNodeNameLast + sNodeName);
if (xN[iNode] == null)
{
xN[iNode] = docConfigPath.CreateNode("element", sNode[iNode], "");
xN[iNode].InnerText = "";
docConfigPath.SelectSingleNode(sNodeNameLast).AppendChild(xN[iNode]);
}
sNodeNameLast += sNodeName;
}
if (sNode[cnt - 1] != "")
xN[iNode - 1].InnerText = sNode[cnt - 1];
return xN[cnt - 2];
}
And the loading is on your:
private void Form1_Load(object sender, EventArgs e)
{
XmlDocument docConfigPath = new XmlDocument();
docConfigPath.Load(XML_Config_Path);
XmlNodeList nodeList = docConfigPath.SelectNodes("config/Size");
Height = ReadNodeInnerTextAsNumber("config/Size/Height", docConfigPath);
Width = ReadNodeInnerTextAsNumber("config/Size/Width", docConfigPath);
Top = ReadNodeInnerTextAsNumber("config/Size/Top", docConfigPath);
Left = ReadNodeInnerTextAsNumber("config/Size/Left", docConfigPath);
}
The config.xml should contain the following:
<?xml version="1.0" encoding="utf-8"?>
<config>
<Size>
<Height>800</Height>
<Width>1400</Width>
<Top>100</Top>
<Left>280</Left>
</Size>
</config>
I've been using this method so far and it's been working great. You don't have to fiddle around with application settings. Instead, it uses serialization to write a settings file to your working directory. I use JSON, but you can use .NET's native XML serialization or any serialization for that matter.
Put these static methods in a common extensions class. Bonus points if you have a common extensions project that you reference by multiple projects:
const string WINDOW_STATE_FILE = "windowstate.json";
public static void SaveWindowState(Form form)
{
var state = new WindowStateInfo
{
WindowLocation = form.Location,
WindowState = form.WindowState
};
File.WriteAllText(WINDOW_STATE_FILE, JsonConvert.SerializeObject(state));
}
public static void LoadWindowState(Form form)
{
if (!File.Exists(WINDOW_STATE_FILE)) return;
var state = JsonConvert.DeserializeObject<WindowStateInfo>(File.ReadAllText(WINDOW_STATE_FILE));
if (state.WindowState.HasValue) form.WindowState = state.WindowState.Value;
if (state.WindowLocation.HasValue) form.Location = state.WindowLocation.Value;
}
public class WindowStateInfo
{
public FormWindowState? WindowState { get; set; }
public Point? WindowLocation { get; set; }
}
You only need to write that code once and never mess with again. Now for the fun part: Put the below code in your form's Load and FormClosing events like so:
private void Form1_Load(object sender, EventArgs e)
{
WinFormsGeneralExtensions.LoadWindowState(this);
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
WinFormsGeneralExtensions.SaveWindowState(this);
}
That is all you need to do. The only setup is getting those extensions into a common class. After that, just add two lines of code to your form's code and you're done.
This code will only really work if your WinForm's app has a single form. If it has multiple forms that you want to remember the positions of, you'll need to get creative and do something like this:
public static void SaveWindowState(Form form)
{
var state = new WindowStateInfo
{
WindowLocation = form.Location,
WindowState = form.WindowState
};
File.WriteAllText($"{form.Name} {WINDOW_STATE_FILE}", JsonConvert.SerializeObject(state));
}
I only save location and state, but you can modify this to remember form height and width or anything else. Just make the change once and it will work for any application that calls it.

Categories