using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Xml; using System.Windows.Forms; using TradeIdeas.XML; /* Typically the main program would make one instance of the Layout Manager. * * Each window type is represented by a string. This string must be saved when you are * saving a window, so we know which procedure to call when restoring a window. The * rest is completely up to the window, but most windows will call SaveBase() and * RestoreBase() to do the common things like the size and position of the window. * The mechanism doesn't even require a form; you can choose to save other settings * as part of the layout. * * If you want to be saved as part of the layout, you have two choices. You can * implement ISaveLayout. When the user askes to save the layout, we iterate though * all open windows looking for windows that implement that interface. That is ideal * for classes which can support any number of windows. We also have our own list of * windows named SpecialSaveLayout. This is useful for windows which are unique, and * which might be hidden at the time we save the layout. These will still have the * chance save their layout. * * Note that the callback for saving your layout gives you an XML node. That is the * parent of any nodes you create. You should create one child per window that you * want to save. This gives you the opportunity to decide at runtime not to save * a specific window. Or to save more than one window in one callback. * * All restoring is done through AddRestoreRule(). The key is the name that we * stored in the XML when we saved the layout. Each key points to a delegate. For * a class which can support any number of windows, this delegate will typically * call the class's constructor, then RestoreBase(), then anything specific for * that window. For unique windows, the delegate will point directly to an existing * window object, so there is no need to call the constructor. The delegate will * immedately call RestoreBase() followed by anything specific to the object. * * Notice that we always store a list of windows. Presumably when you say to save * a window, you will get a file with only that one window. However, there is * no restriction, and when you say to load a window, you might get multiple windows. * In fact, the only difference between loading a saved window and loading a saved * layout is that before we restore an old layout, we close all the existing windows. * * Notice that there is a way to ignore the position when we restore a window. That * makes a convenient way to duplicate a window. Save the window into memory, then * restore everything but the position. Windows should ask the operating system to * put them in the default position when they are first created. (For some reason * there is no method to request the default position after a window has been * created.) When you duplicate a window, you don't want the new to be on top of * the old one. That's confusing to a user; it looks like nothing happened. The * default position will be offset by a little bit each time you create a new window * so you can see each window clearly. * * This code was specifically designed to know nothing about the specific windows * in the program. It learns about them at runtime through callbacks. So many * different main programs should all be able to use this library routine. This * code is based loosely on a similar library I wrote for Delphi. */ namespace TradeIdeas.TIQ { /// /// This delegate works with . This allows /// any class to participate in the layout. is an alternative. /// This delegate works well with classes that are not forms, and with forms where you /// want exactly 0 or 1 instance of each form to be visible. /// /// /// This is the whole layout file. Create one new node for each window. /// provides a good starting point for saving a window. /// public delegate void SaveLayout(XmlNode parent); /// /// A window should implement this interface if it wants to participate in the layout. In particular, /// this is used when there can be 0 or more windows of a particular type. We iterate through the /// list of visible windows looking for windows that implement this. is a /// reasonable alternative. /// public interface ISaveLayout { /// /// Add yourself to the layout. /// provides a good starting point for saving a window. /// /// /// This is the whole layout file. Create one new node for each window. /// void SaveLayout(XmlNode parent); } public delegate void LayoutRestoreCompleteHandler(); public delegate void RestoreLayout(XmlNode description, bool ignorePosition, bool cascadePosition); public class LayoutManager { public const string FORM_TYPE = "FORM_TYPE"; const string BASE = "BASE"; const int CASCADE_OFFSET = 20; public static XmlNode SaveBase(XmlNode parent, Form toSave, string formType) { // Start by creating the new node for the window, as that will be a common // activity. XmlNode result = parent.NewNode("WINDOW"); // FORM_TYPE is required. result.SetProperty(FORM_TYPE, formType); // Put all of these properties in a sub tag, so we keep the namespace clear. // We might want to add new properties later, and we don't want their names // to conflict with names used by the individual windows. XmlNode baseNode = result.NewNode(BASE); // Save the position of the windows. For Mimimized and Maximized windows // use RestoreBounds property to retrieve size and position. if (toSave.WindowState == FormWindowState.Minimized || toSave.WindowState == FormWindowState.Maximized) { baseNode.SetProperty("Top", toSave.RestoreBounds.Top); baseNode.SetProperty("Left", toSave.RestoreBounds.Left); baseNode.SetProperty("Height", toSave.RestoreBounds.Height); baseNode.SetProperty("Width", toSave.RestoreBounds.Width); } else { baseNode.SetProperty("Top", toSave.Top); baseNode.SetProperty("Left", toSave.Left); baseNode.SetProperty("Height", toSave.Height); baseNode.SetProperty("Width", toSave.Width); } baseNode.SetProperty("WindowState", toSave.WindowState); baseNode.SetProperty("Visible", toSave.Visible); return result; } public static void RestoreBase(XmlNode description, Form toRestore, bool ignorePosition, bool cascadePosition) { XmlNode baseNode = description.Node("BASE"); bool visible = baseNode.Property("Visible", true); if (!visible) toRestore.Visible = false; if (!ignorePosition) { if (cascadePosition) { toRestore.Top = baseNode.Property("Top", toRestore.Top) + CASCADE_OFFSET; toRestore.Left = baseNode.Property("Left", toRestore.Left) + CASCADE_OFFSET; } else { toRestore.Top = baseNode.Property("Top", toRestore.Top); toRestore.Left = baseNode.Property("Left", toRestore.Left); } } toRestore.Height = baseNode.Property("Height", toRestore.Height); toRestore.Width = baseNode.Property("Width", toRestore.Width); MakeFullyVisible(toRestore); toRestore.WindowState = baseNode.PropertyEnum("WindowState", toRestore.WindowState); try { // TryParse requires dot net 4.0. FormWindowState windowState = (FormWindowState)Enum.Parse(typeof(FormWindowState), baseNode.Property("WindowState")); toRestore.WindowState = windowState; } catch { // by default keep the current value, i.e. do nothing. } if (visible) // If we want the window to be invisible, do that first. If we want to window to be // visible, do that last. If we are changing the visibity, and we are changing other // attributes (like the size or position) we want the window to be invisible while the // other changes are being made. Experience shows that things look a lot cleaner that // way. toRestore.Visible = true; } public event SaveLayout SpecialSaveLayout; /// /// This is like except that the caller can specify which items /// to include in the file. Remember that a window file and a layout file have exactly /// the same format. If it'w important to know which is which, that information must /// be saved elsewhere. (The traditional "Save Layout..." and "Save Window..." options /// use the file extension to store this. The cloud puts this into a field in the /// database.) /// /// /// If this is true we save the position of the main window, the symbol list window, /// etc. The exact list of what's saved comes from , /// so it is extensible. /// /// /// The list of windows to save. /// /// An XML file describing the layout. This is in a format suitable for sending to . public XmlDocument SaveSome(bool includeSpecial, IEnumerable windows) { XmlDocument result = new XmlDocument(); XmlNode container = result.CreateElement("LAYOUT"); result.AppendChild(container); foreach (ISaveLayout saveable in windows) { if (null != saveable) try { saveable.SaveLayout(container); } catch { } } if (includeSpecial && (null != SpecialSaveLayout)) try { SpecialSaveLayout(container); } catch { } return result; } public XmlDocument SaveAll() { XmlDocument result = new XmlDocument(); XmlNode container = result.CreateElement("LAYOUT"); result.AppendChild(container); foreach (Form form in Application.OpenForms) { ISaveLayout saveable = form as ISaveLayout; if (null != saveable) try { saveable.SaveLayout(container); } catch { } } if (null != SpecialSaveLayout) try { SpecialSaveLayout(container); } catch { } return result; } public void SaveAll(string fileName) { try { SaveAll().Save(fileName); } catch { } } // The output format of SaveOne and SaveAll are the same. The only difference // is that SaveOne has only the one window in it. But it's still set up as a // list of windows, just a list with only one entry. Presumably the main program // will use a different file extension to seperate these two cases, and that // will decide whether closeOldWindows is true or false in the call to Restore(). public void SaveOne(SaveLayout code, string fileName) { XmlDocument result = new XmlDocument(); XmlNode container = result.CreateElement("LAYOUT"); result.AppendChild(container); code(container); try { result.Save(fileName); } catch { } } public void SaveOne(ISaveLayout window, string fileName) { SaveOne(window.SaveLayout, fileName); } public void Restore(XmlNode all, bool closeOldWindows) { if (closeOldWindows) { // Create a copy of the list, because the list will be modified as soon // as we close the first window. List
toClose = new List(); foreach (Form form in Application.OpenForms) if (form is ISaveLayout) toClose.Add(form); foreach (Form form in toClose) { form.Close(); form.Dispose(); } } foreach (XmlNode description in all.Enum()) if (description is XmlElement) try { //Restore position. No cascading RestoreOne(description, false, false); } catch { } if (null != LayoutRestoreComplete) LayoutRestoreComplete(); } public XmlNode GetXmlFromFile(string fileName) { XmlNode body = null; try { XmlDocument document = new XmlDocument(); document.Load(fileName); body = document.DocumentElement; } catch { } return body; } public void Restore(string fileName, bool closeOldWindows) { XmlNode body = null; try { body = GetXmlFromFile(fileName); } catch { } if (null == body) { // Almost certainly an invalid XML file. string message = "Invalid File"; string title = "Error"; MessageBox.Show(message, title, MessageBoxButtons.OK, MessageBoxIcon.Error); } else { // Restore should not throw an exception. However, if it does, we want // to know about it. Don't mix that in with a bad xml file. Restore(body, closeOldWindows); } if (null != LayoutRestoreComplete) LayoutRestoreComplete(); } private void RestoreOne(XmlNode description, bool ignorePosition, bool cascadePosition) { RestoreLayout code; if (_restoreRules.TryGetValue(description.Property(FORM_TYPE), out code)) code(description, ignorePosition, cascadePosition); } private Dictionary _restoreRules = new Dictionary(); public void AddRestoreRule(string windowType, RestoreLayout code) { if (_restoreRules.ContainsKey(windowType)) throw new ArgumentException("Duplicate restore handler for " + windowType); _restoreRules[windowType] = code; } public void Duplicate(ISaveLayout original) { XmlDocument result = new XmlDocument(); XmlNode container = result.CreateElement("TEMP"); result.AppendChild(container); original.SaveLayout(container); // Use and cascade position of duplicate window foreach (XmlNode description in container.ChildNodes) RestoreOne(description, false, true); } // It seemed more convenient to have only one of these. I'm not sure if it even makes // sense to allow more than one. That's certainly not the way we plan to use it. private LayoutManager() { } static private LayoutManager _instance; static public LayoutManager Instance() { if (null == _instance) _instance = new LayoutManager(); return _instance; } public event LayoutRestoreCompleteHandler LayoutRestoreComplete; // Typically the main program would decide on a good place to save layout files, // and the library routines would use that suggestion as the default directory. // This might be null if no suggestion is available. public string Directory { get; set; } public static void MakeFullyVisible(Form form, Screen screen = null) { // This is based on the function of the same name in the Delphi library. if (null == screen) screen = Screen.FromControl(form); if (null == screen) screen = Screen.PrimaryScreen; int left = form.Left; int top = form.Top; if (left + form.Width > screen.WorkingArea.Right) left = screen.WorkingArea.Right - form.Width; if (top + form.Height > screen.WorkingArea.Bottom) top = screen.WorkingArea.Bottom - form.Height; if (top < screen.WorkingArea.Top) top = screen.WorkingArea.Top; if (left < screen.WorkingArea.Left) left = screen.WorkingArea.Left; form.Left = left; form.Top = top; } /* procedure TCustomForm.MakeFullyVisible(AMonitor: TMonitor); var ALeft: Integer; ATop: Integer; begin if AMonitor = nil then AMonitor := Monitor; ALeft := Left; ATop := Top; if Left + Width > AMonitor.Left + AMonitor.Width then ALeft := AMonitor.Left + AMonitor.Width - Width; if Left < AMonitor.Left then ALeft := AMonitor.Left; if Top + Height > AMonitor.Top + AMonitor.Height then ATop := AMonitor.Top + AMonitor.Height - Height; if Top < AMonitor.Top then ATop := AMonitor.Top; SetBounds(ALeft, ATop, Width, Height); end; */ } }