Remember the Screen Location
Problem: | You want your application to remember its location on the screen and restore to that location the next time the app runs. |
Solution: | Although
this task is easy, you need to take into account that when you restore
an application, what used to be on the screen before might not be on the
screen anymore. For example, a user might rearrange a multiple-monitor
scenario, or merely change the resolution of his screen to something
smaller. |
The screen location
should be a user-specific setting. For the following example, two user
settings were created in the standard Settings.settings file.
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
RestoreLocation();
}
private void RestoreLocation()
{
Point location = Properties.Settings.Default.FormLocation;
Size size = Properties.Settings.Default.FormSize;
//make sure location is on a monitor
bool isOnScreen = false;
foreach (Screen screen in Screen.AllScreens)
{
if (screen.WorkingArea.Contains(location))
{
isOnScreen = true;
}
}
//if our window isn't visible, put it on primary monitor
if (!isOnScreen)
{
this.SetDesktopLocation(
Screen.PrimaryScreen.WorkingArea.Left,
Screen.PrimaryScreen.WorkingArea.Top);
}
//if too small, just reset to default
if (size.Width < 10 || size.Height < 10)
{
Size = new Size(300, 300);
}
}
private void SaveLocation()
{
//these are user settings I created in the
//Properties\Settings.settings file
Properties.Settings.Default.FormLocation = this.Location;
Properties.Settings.Default.FormSize = this.Size;
Properties.Settings.Default.Save();
}
protected override void OnClosing(CancelEventArgs e)
{
base.OnClosing(e);
SaveLocation();
}
}
Implement Undo Using Command Objects
Problem: | You want to be able to undo commands in your application. |
Solution: | Most
programs that let the user edit content have the ability to let the
user undo the previous action. This section demonstrates a simple widget
application that allows undo functionality (see Figure 1).
|
The most popular way
to implement this involves command objects that know how to undo
themselves. Every possible action in the program is represented by a
command object.
Note
Not everything the user can
do in your application needs to be a command. For example, moving the
cursor and changing the current selection aren’t usually considered
actions. Generally, undoable commands should be those that change the
user’s data.
Define the Command Interface and History Buffer
Here’s a possible interface:
interface ICommand
{
void Execute();
void Undo();
string Name { get; }
}
We also need a way to track all our commands in the order they were issued:
class CommandHistory
{
private Stack<ICommand> _stack = new Stack<ICommand>();
public bool CanUndo
{
get
{
return _stack.Count > 0;
}
}
public string MostRecentCommandName
{
get
{
if (CanUndo)
{
ICommand cmd = _stack.Peek();
return cmd.Name;
}
return string.Empty;
}
}
public void PushCommand(ICommand command)
{
_stack.Push(command);
}
public ICommand PopCommand()
{
return _stack.Pop();
}
}
Given these two things, the specific implementations of commands depends on the data structures of the application.
In this case, we have an IWidget interface defining all our objects:
interface IWidget
{
void Draw(Graphics graphics);
bool HitTest(Point point);
Point Location { get; set; }
Size Size { get; set; }
Rectangle BoundingBox { get; }
}
Define Command Functionality
One
command we need is to be able to undo a drag/move operation. The
command object needs only as much context to be able to do and undo the
operation (in this case, the old location and the new location):
class MoveCommand : ICommand
{
private Point _originalLocation;
private Point _newLocation;
private IWidget _widget;
public MoveCommand(IWidget widget,
Point originalLocation,
Point newLocation)
{
this._widget = widget;
this._originalLocation = originalLocation;
this._newLocation = newLocation;
}
#region ICommand Members
public void Execute()
{
_widget.Location = _newLocation;
}
public void Undo()
{
_widget.Location = _originalLocation;
}
public string Name
{
get { return "Move widget"; }
}
#endregion
}
Here’s the CreateWidgetCommand object, which takes a different type of state:
class CreateWidgetCommand : ICommand
{
private ICollection<IWidget> _collection;
private IWidget _newWidget;
public CreateWidgetCommand(ICollection<IWidget> collection, IWidget newWidget)
{
_collection = collection;
_newWidget = newWidget;
}
#region ICommand Members
public void Execute()
{
_collection.Add(_newWidget);
}
public void Undo()
{
_collection.Remove(_newWidget);
}
public string Name
{
get { return "Create new widget"; }
}
#endregion
}
To use this functionality, you just have to create the command objects at the appropriate time. Here is the Form from the CommandUndo sample code. Look at the project in Visual Studio to see the full source.
public partial class Form1 : Form
{
private CommandHistory _history = new CommandHistory();
private List<IWidget> _widgets = new List<IWidget>();
private bool _isDragging = false;
private IWidget _dragWidget = null;
private Point _prevMousePt;
private Point _originalLocation;
private Point _newLocation;
public Form1()
{
InitializeComponent();
panelSurface.MouseDoubleClick += new MouseEventHandler(panelSurface_MouseDoubleClick);
panelSurface.Paint += new PaintEventHandler(panelSurface_Paint);
panelSurface.MouseMove +=
new MouseEventHandler(panelSurface_MouseMove);
panelSurface.MouseDown +=
new MouseEventHandler(panelSurface_MouseDown);
panelSurface.MouseUp +=
new MouseEventHandler(panelSurface_MouseUp);
editToolStripMenuItem.DropDownOpening += new EventHandler(editToolStripMenuItem_DropDownOpening);
undoToolStripMenuItem.Click +=
new EventHandler(undoToolStripMenuItem_Click);
}
void panelSurface_MouseDown(object sender, MouseEventArgs e)
{
IWidget widget = GetWidgetUnderPoint(e.Location);
if (widget != null)
{
_dragWidget = widget;
_isDragging = true;
_prevMousePt = e.Location;
_newLocation = _originalLocation = _dragWidget.Location;
}
}
void panelSurface_MouseMove(object sender, MouseEventArgs e)
{
if (!_isDragging)
{
IWidget widget = GetWidgetUnderPoint(e.Location);
if (widget != null)
{
panelSurface.Cursor = Cursors.SizeAll;
}
else
{
panelSurface.Cursor = Cursors.Default;
}
}
else if (_dragWidget != null)
{
Point offset = new Point(e.Location.X - _prevMousePt.X,
e.Location.Y - _prevMousePt.Y);
_prevMousePt = e.Location;
_newLocation.Offset(offset);
//update the widget temporarily as we move
//-- not a command in this case
//because we don't want to record every dragging operation
_dragWidget.Location = _newLocation;
Refresh();
}
}
void panelSurface_MouseUp(object sender, MouseEventArgs e)
{
if (_isDragging)
{
//now perform the command so that Undo restores to location
//before we started dragging
RunCommand(new MoveCommand(_dragWidget,
_originalLocation,
_newLocation));
}
_isDragging = false;
_dragWidget = null;
}
void panelSurface_MouseDoubleClick(object sender, MouseEventArgs e)
{
CreateNewWidget(e.Location);
}
private IWidget GetWidgetUnderPoint(Point point)
{
foreach (IWidget widget in _widgets)
{
if (widget.BoundingBox.Contains(point))
{
return widget;
}
}
return null;
}
void panelSurface_Paint(object sender, PaintEventArgs e)
{
foreach (IWidget widget in _widgets)
{
widget.Draw(e.Graphics);
}
}
//menu handling
void editToolStripMenuItem_DropDownOpening(object sender,
EventArgs e)
{
undoToolStripMenuItem.Enabled = _history.CanUndo;
if (_history.CanUndo)
{
undoToolStripMenuItem.Text = "&Undo "
+ _history.MostRecentCommandName;
}
else
{
undoToolStripMenuItem.Text = "&Undo";
}
}
void undoToolStripMenuItem_Click(object sender, EventArgs e)
{
UndoMostRecentCommand();
}
private void createToolStripMenuItem_Click(object sender,
EventArgs e)
{
CreateNewWidget(new Point(0, 0));
}
private void clearToolStripMenuItem_Click(object sender,
EventArgs e)
{
RunCommand(new DeleteAllWidgetsCommand(_widgets));
Refresh();
}
private void CreateNewWidget(Point point)
{
RunCommand(new CreateWidgetCommand(_widgets,
new Widget(point)));
Refresh();
}
private void RunCommand(ICommand command)
{
_history.PushCommand(command);
command.Execute();
}
private void UndoMostRecentCommand()
{
ICommand command = _history.PopCommand();
command.Undo();
Refresh();
}
}
Note
Although
WPF has the notion of command objects already, they do not have the
ability to undo themselves (which makes sense because undo is an
application-dependent operation). The ideas in this section can be
easily translated to WPF.