unit FormState; { [FormState] [1.8] Delphi 2005 January 2009 LICENSE The contents of this file are subject to the Mozilla Public License Version 1.1 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at "http://www.mozilla.org/MPL/" Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License. The Original Code is "[FormState.pas]". The Initial Developer of the Original Code is Martin Holmes (Victoria, BC, Canada, "http://www.mholmes.com/"). Copyright (C) 2005 Martin Holmes and the University of Victoria Computing and Media Centre. The code was co-developed for university and personal projects, and rights are shared by Martin Holmes and the University of Victoria. All Rights Reserved. } { Class initially created by Martin Holmes Fall 2005, using Delpi 2005, and only tested with Delphi 2005. Extended April 2006 to handle JEDI JVCL Docking components. Extended again January 2008 to allow saving/reloading of the items of combo boxes as well as the index. This enables the saving and loading of form position and size, and the size of any controls with an Align setting, and the settings of various other controls, in an XML or an INI file in the AppData folder. The object also publishes its AppDirPath property so any other code using it can use the same application data folder. Whether XML or INI format is used depends on the second parameter of the Create function, the boolean UseXML. Use it like this: Create a private variable in the form: FFormStateSaver: TFormStateSaver; In the FormShow, do this: FFormStateSaver := TFormStateSaver.Create(Self, True); (This is the simplest constructor; the boolean parameter specifies that XML should be used for storing the data, as opposed to INI file format. A more complex constructor is available, which enables you to set parameters for storing the states of a range of controls. See the code for details.) In the FormClose or the FormDestroy, do this: FreeAndNil(FFormStateSaver); This is specialized for TTntForms (from Troy Wolbrink's Unicode Controls) because that's what I use for all my forms in Delphi 2005. For this library to work properly: 1. Form Position should be DefaultPosOnly. (However, if you are going to show the form modally, set it to poDesigned. If you don't, it will be positioned automatically by the system in the wrong place.) 2. Application Title must be specified (in the project options). 3. The application must include version info. The latter two are required in order to save and load the file reliably. Other properties allow the saving of other data, including the values of edit boxes and combo boxes, and the current directories of dialog boxes. Additional feature: This class can also be used to manage prompting the user to check for application updates. In order to do this, simple set the UpdateURL property to a valid URL after creating the object, then call IsUpdatePromptDue; if it returns True, you can ask the user if they want to check for updates, then send a browser to the URL if they agree. The current version number will be appended as a GET parameter to the URL: [URL]/blah.php?version=1.2.3.4. A server-side script can then do whatever it likes with that info. Once you have prompted the user, set the UpdatePrompted property to True. IsUpdatePromptDue will return True if the last date prompted is more than 30 days ago, AND there is a string in the UpdateURL field. Dependencies: XDOM_4_1 (Dieter Kohler) for reading and writing XML files. VersionInfo (this has a class for getting version info about the running application). It would be feasible to use an instantiated TAppVersionInfo belonging to an instance of TSplashAbout owned by the main form, but then this library wouldn't be portable. GenFunctions (my library containing, among other things, code for getting Special Folder info from Windows). mdhSpin (My SpinEdit control with Unicode Hint property). IniFiles (this could easily be replaced with RegIni or an XML equivalent). TntUnicode libraries (Troy Wolbrink). } interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, Menus, ExtCtrls, StdCtrls, TntForms, TntStdCtrls, TntExtCtrls, VersionInfo, GenFunctions, IniFiles, XDOM_4_1, TntComCtrls, TntSysUtils, Spin, mdhSpin, TntDialogs, TntCheckLst; type TFormStateSaver = class(TObject) private FOwnerForm: TTntForm; FVersionInfo: TAppVersionInfo; FProductID: WideString; FDirPath: WideString; FFilePath: WideString; FXMLFilePath: WideString; FControlList: TList; FDialogList: TList; FUseXML: Boolean; FSaveFormPos: Boolean; FSaveCheckboxStates: Boolean; FSaveSpinEditValues: Boolean; FSaveComboBoxItems: Boolean; FSaveComboBoxStates: Boolean; FSaveRadioGroupStates: Boolean; FSaveEditContents: Boolean; FSaveDialogDirs: Boolean; FPortableMode: Boolean; FUpdateURL: WideString; FdtLastUpdatePrompt: integer; //Set to Date - 30 days on startup; overridden if //there's a setting in the storage file. function FirstRunOfApp: Boolean; procedure SetUpdateURL(inURL: WideString); procedure ReadStateFromINIFile; procedure ReadStateFromXMLFile; procedure WriteStateToINIFile; procedure WriteStateToXMLFile; procedure EnumerateControls(Parent: TComponent); //Utility function for getting dialog box directory to save function GetDialogDir(dlgBox: TOpenDialog): WideString; //Set the dialog box initial dir procedure SetDialogDir(dlgBox: TOpenDialog; wsDir: WideString); procedure GetDirPaths; //Called to find out whether it's time to ask the user whether to update or not. function UpdateCheckDue: Boolean; procedure SetUpdatePrompted(const Value: Boolean); public constructor Create(OwnerForm: TTntForm; UseXML: Boolean); overload; constructor Create(OwnerForm: TTntForm; UseXML: Boolean; inSaveFormPos, inSaveCheckboxStates, inSaveSpinEditValues, inSaveComboBoxItems, inSaveComboBoxStates, inSaveRadioGroupStates, inSaveEditContents, inSaveDialogDirs: Boolean); overload; destructor Destroy; override; procedure ReadStateFromFile; procedure WriteStateToFile; published property AppDirPath: WideString read FDirPath; property UseXML: Boolean read FUseXML write FUseXML default True; property SaveFormPos: Boolean read FSaveFormPos write FSaveFormPos default True; property SaveCheckboxStates: Boolean read FSaveCheckboxStates write FSaveCheckboxStates default False; property SaveSpinEditValues: Boolean read FSaveSpinEditValues write FSaveSpinEditValues default False; property SaveComboBoxItems: Boolean read FSaveComboBoxItems write FSaveComboBoxItems default False; property SaveComboBoxStates: Boolean read FSaveComboBoxStates write FSaveComboBoxStates default False; property SaveRadioGroupStates: Boolean read FSaveRadioGroupStates write FSaveRadioGroupStates default False; property SaveEditContents: Boolean read FSaveEditContents write FSaveEditContents default False; property SaveDialogDirs: Boolean read FSaveDialogDirs write FSaveDialogDirs default False; property IsFirstRunOfApp: Boolean read FirstRunOfApp; property IsUpdateCheckDue: Boolean read UpdateCheckDue; property PortableMode: Boolean read FPortableMode write FPortableMode; property UpdateURL: WideString read FUpdateURL write SetUpdateURL; property UpdatePrompted: Boolean write SetUpdatePrompted; end; implementation { TFormStateSaver } procedure TFormStateSaver.GetDirPaths; var i: integer; wsAppDir: WideString; begin {We need to detect if this application is being run in "portable" mode, of in a regular install. In portable mode, the state info is stored in a directory parallel with the one in which the app is running. If it's in normal mode, we use the regular AppData directory. } PortableMode := False; if ParamCount > 0 then for i := 0 to ParamCount do if ParamStr(i) = '-portable' then PortableMode := True; if (PortableMode = True) then begin FDirPath := WideExpandFileName(Application.ExeName + '\..\..\..\Data'); if not (WideDirectoryExists(FDirPath)) then begin try WideForceDirectories(FDirPath); except //Just silently fail end; end; if not(WideDirectoryExists(FDirPath)) then FDirPath := AppDataFolder + '\' + FVersionInfo.CompanyName + '\' + FProductID;; end else begin FDirPath := AppDataFolder + '\' + FVersionInfo.CompanyName + '\' + FProductID; end; if not (WideDirectoryExists(FDirPath)) then WideForceDirectories(FDirPath); FFilePath := FDirPath + '\' + FOwnerForm.Name + '.ini'; FXMLFilePath := FDirPath + '\' + FOwnerForm.Name + '.xml'; end; constructor TFormStateSaver.Create(OwnerForm: TTntForm; UseXML: Boolean); begin FOwnerForm := OwnerForm; FUseXML := True; FSaveFormPos := True; //this is the normal behaviour //Other option properties default to false in this constructor. FSaveCheckboxStates := False; FSaveSpinEditValues := False; FSaveComboBoxItems := False; FSaveComboBoxStates := False; FSaveRadioGroupStates := False; FSaveEditContents := False; FSaveDialogDirs := False; //Build the application data path for storing the info FVersionInfo := TAppVersionInfo.Create; FProductID := FVersionInfo.ProductName + ' v.' + IntToStr(FVersionInfo.V1) {+ '.' + IntToStr(FVersionInfo.V2)}; GetDirPaths; FControlList := TList.Create; FDialogList := TList.Create; EnumerateControls(FOwnerForm); FUpdateURL := ''; if FirstRunOfApp then FdtLastUpdatePrompt := Integer(Trunc(Date)) else FdtLastUpdatePrompt := Integer(Trunc(Date)) - 30; ReadStateFromFile; end; constructor TFormStateSaver.Create(OwnerForm: TTntForm; UseXML: Boolean; inSaveFormPos, inSaveCheckboxStates, inSaveSpinEditValues, inSaveComboBoxItems, inSaveComboBoxStates, inSaveRadioGroupStates, inSaveEditContents, inSaveDialogDirs: Boolean); begin FOwnerForm := OwnerForm; FUseXML := UseXML; FSaveFormPos := inSaveFormPos; FSaveCheckboxStates := inSaveCheckboxStates; FSaveSpinEditValues := inSaveSpinEditValues; FSaveComboBoxItems := inSaveComboBoxItems; FSaveComboBoxStates := inSaveComboBoxStates; FSaveRadioGroupStates := inSaveRadioGroupStates; FSaveEditContents := inSaveEditContents; FSaveDialogDirs := inSaveDialogDirs; //Build the application data path for storing the info FVersionInfo := TAppVersionInfo.Create; FProductID := FVersionInfo.ProductName + ' v.' + IntToStr(FVersionInfo.V1) {+ '.' + IntToStr(FVersionInfo.V2)}; GetDirPaths; FControlList := TList.Create; FDialogList := TList.Create; EnumerateControls(FOwnerForm); FUpdateURL := ''; if FirstRunOfApp then FdtLastUpdatePrompt := Integer(Trunc(Date)) else FdtLastUpdatePrompt := Integer(Trunc(Date)) - 30; ReadStateFromFile; end; destructor TFormStateSaver.Destroy; begin //These lines added in attempt to find a bug which might result from //controls destroyed during the run of the app. FControlList.Clear; FDialogList.Clear; EnumerateControls(FOwnerForm); WriteStateToFile; FDialogList.Free; FControlList.Free; inherited; end; procedure TFormStateSaver.WriteStateToFile; begin if UseXML then WriteStateToXMLFile else WriteStateToINIFile; end; procedure TFormStateSaver.WriteStateToINIFile; var Ini: TIniFile; i: integer; j: integer; begin //Save the data to an INI file if the app has a directory to put it in. if not DirectoryExists(FDirPath) then Exit; Ini := TIniFile.Create(FFilePath); try if SaveFormPos then begin //Only write position and size if not maximized if not (FOwnerForm.WindowState = wsMaximized) then begin Ini.WriteInteger('Form', 'Left', FOwnerForm.Left); Ini.WriteInteger('Form', 'Top', FOwnerForm.Top); Ini.WriteInteger('Form', 'Width', FOwnerForm.Width); Ini.WriteInteger('Form', 'Height', FOwnerForm.Height); end; Ini.WriteBool('Form', 'Maximized', FOwnerForm.WindowState = wsMaximized); //Store the monitor on which the form is located if Screen.MonitorCount > 0 then Ini.WriteInteger('Form', 'Monitor', FOwnerForm.Monitor.MonitorNum); end; //We only care (at the moment, at any rate) about controls which have an Align //setting which indicates that they're resizable. if FControlList.Count > 0 then for i := 0 to FControlList.Count - 1 do begin if SaveFormPos then begin //List view control needs to save its column widths. Don't save the last one; //that will default to what remains if TControl(FControlList[i]) is TTntListView then with TControl(FControlList[i]) as TTntListView do begin if Columns.Count > 1 then for j := 0 to Columns.Count - 2 do Ini.WriteInteger('Controls', Name + '_Col_' + IntToStr(j), Columns[j].Width); end; //Most panel sizes should be saved, but toolbars must be able to wrap, and status bars should be ignored if not (TControl(FControlList[i]) is TTntToolbar) then with TControl(FControlList[i]) do begin Case Align of alLeft: Ini.WriteInteger('Controls', Name, Width); alTop: Ini.WriteInteger('Controls', Name, Height); //Addition to handle docking panels 18/04/06 alRight: Ini.WriteInteger('Controls', Name, Width); alBottom: Ini.WriteInteger('Controls', Name, Height); end; end; end; //If the property is set, save the state of checkboxes on the form if (TControl(FControlList[i]) is TTntCheckbox) and SaveCheckboxStates then with TTntCheckBox(FControlList[i]) do Ini.WriteBool('Controls', Name, Checked); //If the same property is set, save the state of items in checklistboxes on the form. if (TControl(FControlList[i]) is TTntCheckListBox) and SaveCheckboxStates then with TControl(FControlList[i]) as TTntCheckListBox do begin if Items.Count > 0 then for j := 0 to Items.Count - 1 do Ini.WriteBool('Controls', Name + '_Item_' + IntToStr(j), Checked[j]); end; //If the property is set, save the value of spinedit controls on the form if (TControl(FControlList[i]) is TSpinEdit) and SaveSpinEditValues then with TSpinEdit(FControlList[i]) do Ini.WriteInteger('Controls', Name, Value); //My Unicode spinedit control also needs to be handled if (TControl(FControlList[i]) is TMdhSpinEdit) and SaveSpinEditValues then with TMdhSpinEdit(FControlList[i]) do Ini.WriteInteger('Controls', Name, Value); //If the property is set, save the individual items of the comboboxes on the form if (TControl(FControlList[i]) is TTntComboBox) and SaveComboBoxItems then with TTntComboBox(FControlList[i]) do if Items.Count > 0 then for j := 0 to Items.Count-1 do Ini.WriteString('Controls', Name + '_Item_' + IntToStr(j), Items[j]); //If the property is set, save the itemindex of comboboxes on the form if (TControl(FControlList[i]) is TTntComboBox) and SaveComboBoxStates then with TTntComboBox(FControlList[i]) do Ini.WriteInteger('Controls', Name, ItemIndex); //If the property is set, save the itemindex of radiogroups on the form if (TControl(FControlList[i]) is TTntRadioGroup) and SaveRadioGroupStates then with TTntRadioGroup(FControlList[i]) do Ini.WriteInteger('Controls', Name, ItemIndex); //If the property is set, save the contents of edit controls on the form if (TControl(FControlList[i]) is TTntEdit) and SaveEditContents then with TTntEdit(FControlList[i]) do Ini.WriteString('Controls', Name, Text); end; //Dialog box initial directories, if appropriate if SaveDialogDirs then if FDialogList.Count > 0 then for i := 0 to FDialogList.Count - 1 do Ini.WriteString('Controls', TOpenDialog(FDialogList[i]).Name, GetDialogDir(FDialogList[i])); //Save last update prompt date, if appropriate if Length(FUpdateURL) > 0 then Ini.WriteInteger('App', 'LastUpdatePrompt', FdtLastUpdatePrompt); finally Ini.Free; end; end; procedure TFormStateSaver.WriteStateToXMLFile; var DomImpl: TDomImplementation; DomDoc: TDomDocument; DomToXMLParser: TDomToXMLParser; RootNode: TDomElement; FormNode: TDomElement; ControlsNode: TDomElement; AppNode: TDomElement; ChildNode: TDomElement; i: integer; j: integer; Stream: TFileStream; procedure AppendChildNode(NodeName: WideString; NodeValue: WideString; TheParent: TDomElement); var TextNode: TDomText; begin ChildNode := TDomElement.Create(DomDoc, NodeName); TextNode := TDomText.Create(DomDoc); TextNode.NodeValue := NodeValue; ChildNode.AppendChild(TextNode); TheParent.AppendChild(ChildNode); end; begin //Save the data to an XML file if the app has a directory to put it in. if not DirectoryExists(FDirPath) then Exit; DomImpl := TDomImplementation.Create(nil); try DomDoc := TDomDocument.Create(DomImpl); DomToXMLParser := TDomToXMLParser.Create(nil); try DomToXMLParser.DomImpl := DomImpl; RootNode := TDomElement.Create(DomDoc, 'formState'); DomDoc.AppendChild(RootNode); FormNode := TDomElement.Create(DomDoc, 'form'); RootNode.AppendChild(FormNode); ControlsNode := TDomElement.Create(DomDoc, 'controls'); RootNode.AppendChild(ControlsNode); AppNode := TDomElement.Create(DomDoc, 'app'); RootNode.AppendChild(AppNode); //Add the form data if SaveFormPos then begin //Only write position and size if not maximized if not (FOwnerForm.WindowState = wsMaximized) then begin AppendChildNode('left', IntToStr(FOwnerForm.Left), FormNode); AppendChildNode('top', IntToStr(FOwnerForm.Top), FormNode); AppendChildNode('width', IntToStr(FOwnerForm.Width), FormNode); AppendChildNode('height', IntToStr(FOwnerForm.Height), FormNode); end; AppendChildNode('maximized', BoolToStr(FOwnerForm.WindowState = wsMaximized), FormNode); //Store the monitor on which the form is located if Screen.MonitorCount > 0 then AppendChildNode('monitor', IntToStr(FOwnerForm.Monitor.MonitorNum), FormNode); end; //Now add data about individual controls. //We only care (at the moment, at any rate) about controls which have an Align //setting which indicates that they're resizable. if FControlList.Count > 0 then for i := 0 to FControlList.Count - 1 do begin if SaveFormPos then begin //List view control needs to save its column widths. Don't save the last one; //that will default to what remains if TControl(FControlList[i]) is TTntListView then with TControl(FControlList[i]) as TTntListView do begin if Columns.Count > 1 then for j := 0 to Columns.Count - 2 do AppendChildNode(Name + '_Col_' + IntToStr(j), IntToStr(Columns[j].Width), ControlsNode); end; if not (TControl(FControlList[i]) is TTntToolbar) then with TControl(FControlList[i]) do begin Case Align of alLeft: AppendChildNode(Name, IntToStr(Width), ControlsNode); alTop: AppendChildNode(Name, IntToStr(Height), ControlsNode); //Addition to handle docking panels 18/04/06 alRight: AppendChildNode(Name, IntToStr(Width), ControlsNode); alBottom: AppendChildNode(Name, IntToStr(Height), ControlsNode); end; end; end; //If the property is set, save the state of checkboxes on the form if (TControl(FControlList[i]) is TTntCheckbox) and SaveCheckboxStates then with TTntCheckBox(FControlList[i]) do AppendChildNode(Name, BoolToStr(Checked), ControlsNode); //If the same property is set, save the state of items in checklistboxes on the form. if (TControl(FControlList[i]) is TTntCheckListBox) and SaveCheckboxStates then with TControl(FControlList[i]) as TTntCheckListBox do begin if Items.Count > 0 then for j := 0 to Items.Count - 1 do AppendChildNode(Name + '_Item_' + IntToStr(j), BoolToStr(Checked[j]), ControlsNode); end; //If the property is set, save the values of spinedits on the form if (TControl(FControlList[i]) is TSpinEdit) and SaveSpinEditValues then with TSpinEdit(FControlList[i]) do AppendChildNode(Name, IntToStr(Value), ControlsNode); //Also do my own MdhSpinEdit control if (TControl(FControlList[i]) is TMdhSpinEdit) and SaveSpinEditValues then with TMdhSpinEdit(FControlList[i]) do AppendChildNode(Name, IntToStr(Value), ControlsNode); //If the property is set, save the individual items of the comboboxes on the form if (TControl(FControlList[i]) is TTntComboBox) and SaveComboBoxItems then with TTntComboBox(FControlList[i]) do if Items.Count > 0 then for j := 0 to Items.Count-1 do AppendChildNode(Name + '_Item', Items[j], ControlsNode); //If the property is set, save the itemindex of comboboxes on the form if (TControl(FControlList[i]) is TTntComboBox) and SaveComboBoxStates then with TTntComboBox(FControlList[i]) do AppendChildNode(Name, IntToStr(ItemIndex), ControlsNode); //If the property is set, save the itemindex of radiogroups on the form if (TControl(FControlList[i]) is TTntRadioGroup) and SaveRadioGroupStates then with TTntRadioGroup(FControlList[i]) do AppendChildNode(Name, IntToStr(ItemIndex), ControlsNode); //If the property is set, save the contents of edit controls on the form if (TControl(FControlList[i]) is TTntEdit) and SaveEditContents then with TTntEdit(FControlList[i]) do AppendChildNode(Name, Text, ControlsNode); end; //Now save any dialog box directories, if appropriate. if SaveDialogDirs then if FDialogList.Count > 0 then for i := 0 to FDialogList.Count - 1 do AppendChildNode(TOpenDialog(FDialogList[i]).Name, GetDialogDir(FDialogList[i]), ControlsNode); //Save the last update prompt date, if appropriate. if Length(FUpdateURL) > 0 then AppendChildNode('lastUpdatePrompt', IntToStr(FdtLastUpdatePrompt), AppNode); //Now save the file to disk Stream := TFileStream.Create(FXMLFilePath, fmCreate); try DomToXMLParser.WriteToStream(DomDoc, 'UTF-8', Stream); finally Stream.Free; end; finally DomToXMLParser.Free; end; finally DomImpl.Free; end; end; procedure TFormStateSaver.ReadStateFromFile; begin if UseXML then ReadStateFromXMLFile else ReadStateFromINIFile; end; procedure TFormStateSaver.ReadStateFromINIFile; var Ini: TIniFile; i: integer; j: integer; Mon: integer; wsTemp: WideString; begin //If there's an INI file, read it and set the appropriate properties of the //form and its components. if FileExists(FFilePath) then begin Ini := TIniFile.Create(FFilePath); try if SaveFormPos then begin //First set Window size and position, to place it on the correct monitor //and provide the correct bounds for a splash screen to display if it's //using the form BoundsRect. FOwnerForm.Left := Ini.ReadInteger('Form', 'Left', FOwnerForm.Left); FOwnerForm.Top := Ini.ReadInteger('Form', 'Top', FOwnerForm.Top); FOwnerForm.Width := Ini.ReadInteger('Form', 'Width', FOwnerForm.Width); FOwnerForm.Height := Ini.ReadInteger('Form', 'Height', FOwnerForm.Height); FOwnerForm.Update; //Now handle the situation where the bounds are outside the display available //(which might happen if a second monitor was removed from the system.) if FOwnerForm.Left > (Screen.DesktopWidth - 50) then FOwnerForm.Left := 0; if FOwnerForm.Top > (Screen.DesktopHeight - 50) then FOwnerForm.Top := 0; if Ini.ReadBool('Form', 'Maximized', False) = True then begin //Check which monitor it was on, if multi-monitor Mon := 0; if Screen.MonitorCount > 0 then Mon := Ini.ReadInteger('Form', 'Monitor', 0); if (Mon > 0) and (Mon < Screen.MonitorCount) then FOwnerForm.MakeFullyVisible(Screen.Monitors[Mon]); //Now maximize the window FOwnerForm.WindowState := wsMaximized; FOwnerForm.Update; end; end; if FControlList.Count > 0 then for i := 0 to FControlList.Count - 1 do begin if SaveFormPos then begin if not (TControl(FControlList[i]) is TTntToolbar) then with TControl(FControlList[i]) do begin //We only care (at the moment, at any rate) about controls which have an Align //setting which indicates that they're resizable. Case Align of alLeft: Width := Ini.ReadInteger('Controls', Name, Width); alTop: Height := Ini.ReadInteger('Controls', Name, Height); //Addition to handle docking panels 18/04/06 alRight: Width := Ini.ReadInteger('Controls', Name, Width); alBottom: Height := Ini.ReadInteger('Controls', Name, Height); end; end; //List view control needs to reload its column widths. Don't bother with the last one; //that will default to what remains if TControl(FControlList[i]) is TTntListView then with TControl(FControlList[i]) as TTntListView do begin if Columns.Count > 1 then for j := 0 to Columns.Count - 2 do Columns[j].Width := Ini.ReadInteger('Controls', Name + '_Col_' + IntToStr(j), Columns[j].Width); //Trigger a resize event -- usually needed to shuffle the column widths to avoid scrollbars if Assigned(onResize) then begin onResize(nil); Application.ProcessMessages; end; end; end; //If the property is set, read the state of checkboxes on the form if (TControl(FControlList[i]) is TTntCheckbox) and SaveCheckboxStates then with TTntCheckBox(FControlList[i]) do Checked := Ini.ReadBool('Controls', Name, Checked); //If the same property is set, reload the state of checklistbox items if (TControl(FControlList[i]) is TTntCheckListbox) and SaveCheckboxStates then with TTntCheckListBox(FControlList[i]) do if Items.Count > 0 then for j := 0 to Items.Count-1 do Checked[j] := Ini.ReadBool('Controls', Name + '_Item_' + IntToStr(j), Checked[j]); //If the property is set, read the values of spinedits on the form if (TControl(FControlList[i]) is TSpinEdit) and SaveSpinEditValues then with TSpinEdit(FControlList[i]) do Value := Ini.ReadInteger('Controls', Name, Value); //Also do my own MdhSpinEdit control if (TControl(FControlList[i]) is TMdhSpinEdit) and SaveSpinEditValues then with TMdhSpinEdit(FControlList[i]) do Value := Ini.ReadInteger('Controls', Name, Value); //If the property is set, read the values of items of combo boxes on the form if (TControl(FControlList[i]) is TTntComboBox) and SaveComboBoxItems then with TTntComboBox(FControlList[i]) do begin Items.Clear; j := 0; wsTemp := Ini.ReadString('Controls', Name+'_Item_' + IntToStr(j), ''); while wsTemp <> '' do begin Items.Add(wsTemp); wsTemp := Ini.ReadString('Controls', Name+'_Item_' + IntToStr(j), ''); end; end; //If the property is set, read the itemindex of combo boxes on the form if (TControl(FControlList[i]) is TTntComboBox) and SaveComboBoxStates then with TTntComboBox(FControlList[i]) do ItemIndex := Ini.ReadInteger('Controls', Name, ItemIndex); //If the property is set, read the itemindex of radiogroups on the form if (TControl(FControlList[i]) is TTntRadioGroup) and SaveRadioGroupStates then with TTntRadioGroup(FControlList[i]) do ItemIndex := Ini.ReadInteger('Controls', Name, ItemIndex); //If the property is set, read the text of edit controls on the form if (TControl(FControlList[i]) is TTntEdit) and SaveEditContents then with TTntEdit(FControlList[i]) do Text := Ini.ReadString('Controls', Name, Text); end; //Dialog box initial directories, if appropriate if SaveDialogDirs then if FDialogList.Count > 0 then for i := 0 to FDialogList.Count - 1 do begin wsTemp := Ini.ReadString('Controls', TOpenDialog(FDialogList[i]).Name, ''); SetDialogDir(TOpenDialog(FDialogList[i]), wsTemp); end; //Always try to read last update prompt date. FdtLastUpdatePrompt := Ini.ReadInteger('App', 'LastUpdatePrompt', FdtLastUpdatePrompt); finally Ini.Free; end; end; end; procedure TFormStateSaver.ReadStateFromXMLFile; var i: integer; j: integer; Path: TFileName; DomImpl: TDomImplementation; DomDoc: TDomDocument; XMLToDomParser: TXMLToDomParser; El: TDomElement; boolTemp: Boolean; wsTemp: WideString; wsBool: WideString; wsInt: WideString; Mon: integer; begin if not FileExists(FXMLFilePath) then Exit; Path := TFileName(FXMLFilePath); DomImpl := TDomImplementation.Create(nil); try XMLToDomParser := TXMLToDomParser.Create(nil); try XMLToDomParser.DOMImpl := DomImpl; //Updated code for XDOM 4.1 DomDoc := XMLToDomParser.ParseFile(Path, False); // DomDoc := XMLToDomParser.FileToDom(Path); finally XMLToDomParser.Free; end; //Now read the data from the dom. First get the form element. if SaveFormPos then begin if DomDoc.GetElementsByTagName('form').Length > 0 then begin El := TDomElement(DomDoc.GetElementsByTagName('form').Item(0)); with FOwnerForm do begin if El.GetElementsByTagName('left').Length > 0 then Left := StrToIntDef(El.GetElementsByTagName('left').Item(0).textContent, Left); if El.GetElementsByTagName('top').Length > 0 then Top := StrToIntDef(El.GetElementsByTagName('top').Item(0).textContent, Top); if El.GetElementsByTagName('width').Length > 0 then Width := StrToIntDef(El.GetElementsByTagName('width').Item(0).textContent, Width); if El.GetElementsByTagName('height').Length > 0 then Height := StrToIntDef(El.GetElementsByTagName('height').Item(0).textContent, Height); if El.GetElementsByTagName('maximized').Length > 0 then begin BoolTemp := StrToBoolDef(El.GetElementsByTagName('maximized').Item(0).textContent, False); if BoolTemp then begin //Check which monitor it was on, if multi-monitor Mon := 0; if Screen.MonitorCount > 0 then begin if El.GetElementsByTagName('monitor').Length > 0 then Mon := StrToIntDef(El.GetElementsByTagName('monitor').Item(0).textContent, 0); end; if (Mon > 0) and (Mon < Screen.MonitorCount) then FOwnerForm.MakeFullyVisible(Screen.Monitors[Mon]); //Now maximize the window FOwnerForm.WindowState := wsMaximized; FOwnerForm.Update; end; end; end; end; end; if FControlList.Count > 0 then for i := 0 to FControlList.Count - 1 do begin if SaveFormPos then begin if not (TControl(FControlList[i]) is TTntToolbar) then with TControl(FControlList[i]) do begin //We only care (at the moment, at any rate) about controls which have an Align //setting which indicates that they're resizable. Case Align of alLeft: begin if DomDoc.GetElementsByTagName(Name).Length > 0 then Width := StrToIntDef(DomDoc.GetElementsByTagName(Name).Item(0).textContent, Width); end; alTop: begin if DomDoc.GetElementsByTagName(Name).Length > 0 then Height := StrToIntDef(DomDoc.GetElementsByTagName(Name).Item(0).textContent, Height); end; //Addition to handle docking panels 18/04/06 alRight: begin if DomDoc.GetElementsByTagName(Name).Length > 0 then Width := StrToIntDef(DomDoc.GetElementsByTagName(Name).Item(0).textContent, Width); end; alBottom: begin if DomDoc.GetElementsByTagName(Name).Length > 0 then Height := StrToIntDef(DomDoc.GetElementsByTagName(Name).Item(0).textContent, Height); end; end; end; //List view control needs to reload its column widths. Don't bother with the last one; //that will default to what remains if TControl(FControlList[i]) is TTntListView then with TControl(FControlList[i]) as TTntListView do begin if Columns.Count > 1 then for j := 0 to Columns.Count - 2 do begin wsTemp := Name + '_Col_' + IntToStr(j); if DomDoc.GetElementsByTagName(wsTemp).Length > 0 then begin Columns[j].Width := StrToIntDef(DomDoc.GetElementsByTagName(wsTemp).Item(0).textContent, Columns[j].Width); //Trigger a resize event -- usually needed to shuffle the column widths to avoid scrollbars if Assigned(onResize) then begin onResize(nil); Application.ProcessMessages; end; end; end; end; end; //If the property is set, read the state of checkboxes on the form if (TControl(FControlList[i]) is TTntCheckbox) and SaveCheckboxStates then with TTntCheckBox(FControlList[i]) do if DomDoc.GetElementsByTagName(Name).Length > 0 then begin wsBool := DomDoc.GetElementsByTagName(Name).Item(0).textContent; Checked := StrToBoolDef(string(wsBool), Checked); end; //If the same property is set, reload the state of checklistbox items if (TControl(FControlList[i]) is TTntCheckListbox) and SaveCheckboxStates then with TTntCheckListBox(FControlList[i]) do if Items.Count > 0 then for j := 0 to Items.Count-1 do begin wsTemp := Name + '_Item_' + IntToStr(j); if DomDoc.GetElementsByTagName(wsTemp).Length > 0 then Checked[j] := StrToBoolDef(DomDoc.GetElementsByTagName(wsTemp).Item(0).textContent, Checked[j]); end; //If the property is set, read the values of spinedits on the form if (TControl(FControlList[i]) is TSpinEdit) and SaveSpinEditValues then with TSpinEdit(FControlList[i]) do if DomDoc.GetElementsByTagName(Name).Length > 0 then begin wsInt := DomDoc.GetElementsByTagName(Name).Item(0).textContent; Value := StrToIntDef(string(wsInt), Value); end; //Also handle my own MdhSpinEdit control if (TControl(FControlList[i]) is TMdhSpinEdit) and SaveSpinEditValues then with TMdhSpinEdit(FControlList[i]) do if DomDoc.GetElementsByTagName(Name).Length > 0 then begin wsInt := DomDoc.GetElementsByTagName(Name).Item(0).textContent; Value := StrToIntDef(string(wsInt), Value); end; //If the property is set, read the individual items of combo boxes on the form if (TControl(FControlList[i]) is TTntComboBox) and SaveComboBoxItems then with TTntComboBox(FControlList[i]) do if DomDoc.GetElementsByTagName(Name + '_Item').Length > 0 then begin Items.Clear; for j := 0 to DomDoc.GetElementsByTagName(Name + '_Item').Length-1 do Items.Add(DomDoc.GetElementsByTagName(Name + '_Item').Item(j).textContent); end; //If the property is set, read the itemindex of combo boxes on the form if (TControl(FControlList[i]) is TTntComboBox) and SaveComboBoxStates then with TTntComboBox(FControlList[i]) do if DomDoc.GetElementsByTagName(Name).Length > 0 then begin wsInt := DomDoc.GetElementsByTagName(Name).Item(0).textContent; ItemIndex := StrToIntDef(string(wsInt), ItemIndex); end; //If the property is set, read the itemindex of radiogroups on the form if (TControl(FControlList[i]) is TTntRadioGroup) and SaveRadioGroupStates then with TTntRadioGroup(FControlList[i]) do if DomDoc.GetElementsByTagName(Name).Length > 0 then begin wsInt := DomDoc.GetElementsByTagName(Name).Item(0).textContent; ItemIndex := StrToIntDef(string(wsInt), ItemIndex); end; //If the property is set, read the text of edit controls on the form if (TControl(FControlList[i]) is TTntEdit) and SaveEditContents then with TTntEdit(FControlList[i]) do if DomDoc.GetElementsByTagName(Name).Length > 0 then Text := DomDoc.GetElementsByTagName(Name).Item(0).textContent; end; //Now get any dialog box directories, if appropriate. if SaveDialogDirs then if FDialogList.Count > 0 then for i := 0 to FDialogList.Count - 1 do if DomDoc.GetElementsByTagName(TOpenDialog(FDialogList[i]).Name).Length > 0 then begin wsTemp := DomDoc.GetElementsByTagName(TOpenDialog(FDialogList[i]).Name).Item(0).textContent; SetDialogDir(TOpenDialog(FDialogList[i]), wsTemp); end; //Always try to read last update prompt date. if DomDoc.GetElementsByTagName('lastUpdatePrompt').Length > 0 then begin wsTemp := DomDoc.GetElementsByTagName('lastUpdatePrompt').Item(0).textContent; FdtLastUpdatePrompt := StrToInt(wsTemp); end; finally DomImpl.Free; end; end; procedure TFormStateSaver.EnumerateControls(Parent: TComponent); var i: integer; begin //Get a list of components owned by the component (recursive). if Parent.ComponentCount > 0 then for i := 0 to Parent.ComponentCount-1 do begin if Parent.Components[i] is TControl then FControlList.Add(Parent.Components[i]); if Parent.Components[i] is TOpenDialog then FDialogList.Add(Parent.Components[i]); EnumerateControls(Parent.Components[i]); end; end; function TFormStateSaver.FirstRunOfApp: Boolean; begin //If the form state has been saved before, then this is not the first //run of the application. Result := True; if FileExists(FXMLFilePath) then Result := False else if FileExists(FFilePath) then Result := False; end; function TFormStateSaver.GetDialogDir(dlgBox: TOpenDialog): WideString; begin Result := ''; if dlgBox is TTntOpenDialog then begin with TTntOpenDialog(dlgBox) do begin if WideDirectoryExists(WideExtractFilePath(FileName)) then Result := WideExtractFilePath(FileName) else if WideDirectoryExists(InitialDir) then Result := InitialDir; end; end else begin with TOpenDialog(dlgBox) do begin if DirectoryExists(ExtractFilePath(FileName)) then Result := ExtractFilePath(FileName) else if DirectoryExists(InitialDir) then Result := InitialDir; end; end; end; procedure TFormStateSaver.SetDialogDir(dlgBox: TOpenDialog; wsDir: WideString); begin if Length(wsDir) < 3 then Exit; if wsDir[Length(wsDir)] = WideChar('/') then wsDir := Copy(wsDir, 1, Length(wsDir)-1); if dlgBox is TTntOpenDialog then begin if WideDirectoryExists(wsDir) then TTntOpenDialog(dlgBox).InitialDir := wsDir; end else begin if DirectoryExists(wsDir) then TOpenDialog(dlgBox).InitialDir := string(wsDir); end; end; procedure TFormStateSaver.SetUpdateURL(inURL: WideString); begin {This is a method rather than a direct assignment so that we can add some kind of verification code if we wish.} if (Length(inURL) > 0) then begin FUpdateURL := inURL + '?version=' + FVersionInfo.DottedVersion; end; end; function TFormStateSaver.UpdateCheckDue: Boolean; begin Result := False; //Default if (Length(FUpdateURL) > 0) then if ((Date - 30) > FdtLastUpdatePrompt) then Result := True; end; procedure TFormStateSaver.SetUpdatePrompted(const Value: Boolean); begin if Value = True then FdtLastUpdatePrompt := Integer(Trunc(Date)); end; end.