Tuesday, May 27, 2008

Do not give the OK button a keyboard accelerator

Raymond Chen, a well-known developer on the Windows Shell team at Microsoft, mentioned a principle about dialog design in his blog: Do not give the OK button a keyboard accelerator [1].

But it is funny, that even their own products do not follow this principle strictly. A remarkable example is the "Restart Prompt Dialog of Automatic Updates" in WindowsXP. Both buttons "Restart Now" and "Restart Later" have a keyboard accelerator. If this dialog pops up, while I am pressing ALT+N, the current Windows session will be terminated immediately. And there is no Postpone button either. This button appears in Windows Vista, but there is no option "Do not prompt me again, I will restart my computer later.". If I do not intend to restart my computer very soon, I have to press "Postpone" in every 4 hours. It is really annoying! Not only the unfriendly design, but also infinite critical Windows updates. The only one option for me is to turn off "Automatic Updates".

According to the great design principles [2]: I suggest remove those keyboard accelerators and add a 10-second-delay before restarting Windows, so that user can cancel this action. This is called user experience!

References

  1. Gentle reminder: On a dialog box, do not give OK and Cancel accelerators
  2. How to Design a Great User Experience
 

Monday, May 26, 2008

Form disappeared

After I started working on dual monitors, I have found another interesting issue:

  1. Create a new application with a main form and a sub-form.
  2. Set the property Position of the sub-form to poMainFormCenter
  3. Create a button "Show Sub-Form" on main form.
  4. Move more than 50% area of the main form out of the main screen.
  5. Click the button, we created.
The sub-form disappeared! Because the popup position of sub-form is out of the screen. I did a similar test with Internet Explorer. Its dialog will be shown at the corner of the screen. It can be regarded as a bug, so I have reported this issue to QualityCentral [1] as well.

Solution

This time you have to modify Forms.pas
  1. Copy Forms.pas to your project folder,
  2. find and replace with the following code,
  3. and then recompile your project again.

{$IFDEF FIXUP_FORM_POPUP_POSITION}
function GetPrimaryMonitor: TMonitor;
var
I: Integer;
begin
for I := 0 to Screen.MonitorCount - 1 do
begin
Result := Screen.Monitors[I];
if Result.Primary then
Exit;
end;
Result := Screen.Monitors[0];
end;

procedure CenterFormToPrimaryMonitorCenter(var X, Y: Integer;
AForm: TCustomForm);
var
R: TRect;
begin
R := GetPrimaryMonitor.WorkareaRect;
X := R.Left + (R.Right - R.Left - AForm.Width) div 2;
Y := R.Top + (R.Bottom - R.Top - AForm.Height) div 2;
end;

procedure SnapFormToMonitorEdgeOnDemand(var X, Y: Integer;
AForm: TCustomForm);
var
DistanceLeft, DistanceRight: Integer;
CenterMon: TMonitor;
R: TRect;
begin
CenterMon := Screen.MonitorFromPoint(
Point(X + AForm.Width div 2, Screen.DesktopHeight div 2));
R := CenterMon.WorkareaRect;
// Adjust X-pos
DistanceLeft := X - R.Left;
DistanceRight := R.Right - (X + AForm.Width);
if (DistanceLeft < 0) and (DistanceRight < 0) then
begin
if DistanceLeft >= DistanceRight then
X := R.Left // Snap to left edge
else X := R.Right - AForm.Width; // Snap to right edge
end
else if (DistanceLeft < 0) or (DistanceRight < 0) then
begin
if DistanceLeft < 0 then
X := R.Left // Snap to left edge
else X := R.Right - AForm.Width; // Snap to right edge
end;
// Adjust Y-pos
if Y < R.Top then
Y := R.Top
else if Y > R.Bottom - AForm.Height then
Y := R.Bottom - AForm.Height;
end;
{$ENDIF}

procedure TCustomForm.CMShowingChanged(var Message: TMessage);
// ...
if (FPosition = poScreenCenter) or
((FPosition = poMainFormCenter) and (FormStyle = fsMDIChild)) then
begin
if FormStyle = fsMDIChild then
begin
X := (Application.MainForm.ClientWidth - Width) div 2;
Y := (Application.MainForm.ClientHeight - Height) div 2;
end else
begin
{$IFDEF FIXUP_FORM_POPUP_POSITION}
CenterFormToPrimaryMonitorCenter(X, Y, Self);
{$ELSE}
X := (Screen.Width - Width) div 2;
Y := (Screen.Height - Height) div 2;
{$ENDIF}
end;
{$IFDEF FIXUP_FORM_POPUP_POSITION}
SnapFormToMonitorEdgeOnDemand(X, Y, Self);
SetBounds(X, Y, Width, Height);
//SetWindowToMonitor() will cause unexpected popup position change!
{$ELSE}
if X < Screen.DesktopLeft then
X := Screen.DesktopLeft;
if Y < Screen.DesktopTop then
Y := Screen.DesktopTop;
SetBounds(X, Y, Width, Height);
if Visible then SetWindowToMonitor;
{$ENDIF}
end
else if FPosition in [poMainFormCenter, poOwnerFormCenter] then
begin
CenterForm := Application.MainForm;
if (FPosition = poOwnerFormCenter) and (Owner is TCustomForm) then
CenterForm := TCustomForm(Owner);
if Assigned(CenterForm) then
begin
X := ((CenterForm.Width - Width) div 2) + CenterForm.Left;
Y := ((CenterForm.Height - Height) div 2) + CenterForm.Top;
end else
begin
{$IFDEF FIXUP_FORM_POPUP_POSITION}
CenterFormToPrimaryMonitorCenter(X, Y, Self);
{$ELSE}
X := (Screen.Width - Width) div 2;
Y := (Screen.Height - Height) div 2;
{$ENDIF}
end;
{$IFDEF FIXUP_FORM_POPUP_POSITION}
SnapFormToMonitorEdgeOnDemand(X, Y, Self);
SetBounds(X, Y, Width, Height);
//SetWindowToMonitor() will cause unexpected popup position change!
{$ELSE}
if X < Screen.DesktopLeft then
X := Screen.DesktopLeft;
if Y < Screen.DesktopTop then
Y := Screen.DesktopTop;
SetBounds(X, Y, Width, Height);
if Visible then SetWindowToMonitor;
{$ENDIF}
end
else if FPosition = poDesktopCenter then
// ...
end;

Conclusion

This issue is nothing. The motivation of this article [2] is to suggest you figure out the differences among DesktopRect, BoundsRect and WorkAreaRect. They are almost the same on one monitor environment. But they are totally different on dual monitors, especially when TaskBar is not at its default position.

References

  1. Related discuss at QualityCentral.
  2. The Chinese version of this article (on my blog @csdn)