Disable form restoring on title doubleclick - delphi

Create an empty Delphi VCL project
Remove all BorderIcons of main form
Set WindowState to wsMaximized
Run application. Main window appears maximized.
Double click on window title. Main window restores it's size and there is no possibility to maximize it again.
How to prevent window restoring on title double click without hiding title bar?

You can intercept the restore and additionally the move system commands to prevent restoring by dragging the caption.
type
TForm1 = class(TForm)
protected
procedure WMSysCommand(var Message: TWMSysCommand); message WM_SYSCOMMAND;
...
procedure TForm1.WMSysCommand(var Message: TWMSysCommand);
begin
case Message.CmdType and $FFF0 of
SC_MOVE, SC_RESTORE: Exit;
end;
inherited;
end;

I tested some solutions and the one that worked was:
Set Align property to alClient;
Remove biMaximize from BorderIcons property;
Let WindowState wsNormal (default).

Answer has been edited!
If I got you right, you might want to forbid double-click by title bar in order to prevent restoring form to its original size. You can do that by intercepting WM_NCLBUTTONDBLCLK.
In the example below I've overridden WndProc method of main form and hook forementioned message.
procedure TForm1.WndProc(var Message: TMessage);
begin
case Message.Msg of
WM_NCLBUTTONDBLCLK:
begin
case TWMNCHitMessage(Message).HitTest of
HTCAPTION:
Exit
else // Another HitTest-codes are handled here
Inherited WndProc(Message);
end;
end
else
Inherited WndProc(Message);
end;
end;
Important note
Although now you cannot restore maximized form by double-clicking, it still can be restored just by moving it while mouse is captured by title bar. At least, on Windows 7 this effect is presented.
Steps to reproduce:
Run application;
Press left mouse button while it hovered over title bar;
Don't release LMB and move mouse softly - now form restores its size.
Addendum
Fixed bug with incorrect handling another non-client HitTest-codes excepting HTCAPTION (thanks to user Dsm for pointing this out!).

Related

Displaying a disabled modal form

I'm trying to disable a TForm's descendant and showing it as a modal form.
procedure TForm1.Button1Click(Sender: TObject);
var
Frm : TMyForm;
begin
Frm := TMyForm.Create(nil);
try
Frm.Enabled := False;
Frm.ShowModal();
finally
Frm.Free;
end;
end;
At runtime, it raises the following error message:
Cannot make a visible window modal.
The OP wants to display a disabled form modally when the form should be displayed for read-only purposes.
Disabling the form is the wrong thing to do.
How do you display the information? If you are using TEdit, TMemo, or TRichEdit controls, you should simply set them to read only. Otherwise, if you have some combinations of various controls like radio buttons, you should disable each and every such control, not the form itself. I mean, surely you still want the Cancel button to be enabled?
In addition, disabling the form instead of the actual controls will make the controls look enabled, which is very confusing! That's an important point.
So what you need to do is to display the form normally (not disabled!) and then set its controls to their appropriate states when the dialog is shown.
Just to emphasise my point about disabling the form vs its controls, consider this dialog box:
If I do
procedure TCustomViewFrm.FormShow(Sender: TObject);
begin
Enabled := False;
end;
then it looks like this when shown:
As you can see, every control looks very enabled indeed, but no control responds to mouse or keyboard input. This is very confusing and a horribly bad UX.
In fact, you cannot even close the dialog box using its title-bar Close button or Alt+F4. You cannot close it using its system menu, either. In fact, you cannot close it at all, because to close a window, it must respond to user input, and a disabled window doesn't do that. (You cannot move the window, either.)
Instead, if we disable all controls (except the Cancel button),
procedure DisableControl(AControl: TWinControl);
begin
for var i := 0 to AControl.ControlCount - 1 do
begin
if
(AControl.Controls[i] is TCustomButton)
and
(TCustomButton(AControl.Controls[i]).ModalResult = mrCancel)
then
Continue;
if AControl.Controls[i] is TWinControl then
DisableControl(TWinControl(AControl.Controls[i]));
AControl.Controls[i].Enabled := False;
end;
end;
procedure TCustomViewFrm.FormShow(Sender: TObject);
begin
DisableControl(Self);
end;
you get this nice UI:
Not only is it very clear that all controls are disabled, the user can also close the dialog box without killing your application using the Task Manager.

Disabling the form still allow childs controls to receive input

I'm having a lot of headache in the last days with delphi, what im trying to do is a lot simple, block the interface at somepoint and enable after some other point.
But as simply as it sound i couldn't figure out why somethings are allowed by design, so to clarify:
1) create a project
2) in the form put a edit and a button, tab order of the edit must be first
3) configure the OnExit event of the edit and write:
Enabled := False;
4) configure the OnClick event of the button and write:
ShowMessage('this is right?');
basically this is it, now compile, the focus it will be at the edit, press tab and the form will be disabled as we demanded, so accordingly to the tab order the next control to gain focus is the button (but we disabled the form), now press space and the message should come up.
so the question is: is this right? whats the logical explanation to this behaviour?
thx in advance.
Both TButton and TEdit are TWinControl descendents - this means that they are windowed controls. When they are created they are allocated their own HWND and the operating system posts messages to them directly when they have focus. Disabling their containing form prevents the main form from receiving input messages or from receiving focus but it does not disable any other windowed control if it already has input focus.
If these controls do not have input focus, it is responsibility of the containing form to transfer input focus to them when user input (click, tab key, etc) dictates. If the form is disabled and these controls are not focused then the form will not receive the input messages that would allow it to transfer focus. If focus is transferred to a windowed control, however, then all user input goes directly to that control, even if their parent control's window is disabled - they are in fact their own separate windows.
I'm not sure the behaviour you have observed is a bug - it is perhaps not expected, but it is standard behaviour. There is generally no expectation that disabling one window will also disable others within the same application.
The problem is that there are two separate hierarchies in play. On the VCL level, the Button is a child control and has a parent (the form). On the OS level, however, both are separate windows and the (component level) parent/child relationship is not known to the OS. This would be a similar situation :
procedure TForm1.Button1Click(Sender: TObject);
var
form2 : TForm1;
begin
self.Enabled := false;
form2 := TForm1.Create(self);
try
form2.ShowModal;
finally
form2.Free;
end;
end;
Would you really expect form2 to be disabled when it was shown, simply because its TComponent owner is Form1? Surely not. Windowed controls are much the same.
Windows themselves can also have a parent/child relationship, but this is separate from component ownership (VCL parent/child) and does not necessarily behave in the same way. From MSDN:
The system passes a child window's input messages directly to the
child window; the messages are not passed through the parent window.
The only exception is if the child window has been disabled by the
EnableWindow function. In this case, the system passes any input
messages that would have gone to the child window to the parent window
instead. This permits the parent window to examine the input messages
and enable the child window, if necessary.
Emphasis mine - if you disable a child window then its messages will be routed to the parent for an opportunity to inspect and act upon them. The reverse is not true - a disabled parent will not prevent a child from receiving messages.
A rather tedious workaround could be to make your own set of TWinControls that behave like this :
TSafeButton = class(TButton)
protected
procedure WndProc(var Msg : TMessage); override;
end;
{...}
procedure TSafeButton.WndProc(var Msg : TMessage);
function ParentForm(AControl : TWinControl) : TWinControl;
begin
if Assigned(AControl) and (AControl is TForm) then
result := AControl
else
if Assigned(AControl.Parent) then
result := ParentForm(AControl.Parent)
else result := nil;
end;
begin
if Assigned(ParentForm(self)) and (not ParentForm(self).Enabled) then
Msg.Result := 0
else
inherited;
end;
This walks up the VCL parent tree until it finds a form - if it does and the form is disabled then it rejects input to the windowed control as well. Messy, and probably could be more selective (maybe some messages should not be ignored...) but it would be the start of something that could work.
Digging further, this does seem to be at odds with the documentation :
Only one window at a time can receive keyboard input; that window is
said to have the keyboard focus. If an application uses the
EnableWindow function to disable a keyboard-focus window, the window
loses the keyboard focus in addition to being disabled. EnableWindow
then sets the keyboard focus to NULL, meaning no window has the focus.
If a child window, or other descendant window, has the keyboard focus,
the descendant window loses the focus when the parent window is
disabled. For more information, see Keyboard Input.
This does not seem to happen, even explicitly setting the button's window to be a child with :
oldParent := WinAPI.Windows.SetParent(Button1.Handle, Form1.Handle);
// here, in fact, oldParent = Form1.Handle, so parent/child HWND
// relationship is correct by default.
A bit more (for repro) - same scenario Edit tabs focus to button, exit handler enables TTimer. Here the form is disabled, but the button retains focus even though this seems to confirm that Form1's HWND is indeed the parent window of the button and it should lose focus.
procedure TForm1.Timer1Timer(Sender: TObject);
var
h1, h2, h3 : cardinal;
begin
h1 := GetFocus; // h1 = Button1.Handle
h2 := GetParent(h1); // h2 = Form1.Handle
self.Enabled := false;
h3 := GetFocus; // h3 = Button1.Handle
end;
In the case where we move the button into a panel, everything seems to work (mostly) as expected. The panel is disabled and the button loses focus, but focus then moves to the parent form (WinAPI suggests it should be NULL).
procedure TForm1.Timer1Timer(Sender: TObject);
var
h1, h2, h3 : cardinal;
begin
h1 := GetFocus; // h1 = Button1.Handle
h2 := GetParent(h1); // h2 = Panel1.Handle
Panel1.Enabled := false;
h3 := GetFocus; // h3 = Form1.Handle
end;
Part of the problem seems to be here - it looks like the top form itself is taking responsibility for defocusing controls. This works except when the form itself is the one being disabled :
procedure TWinControl.CMEnabledChanged(var Message: TMessage);
begin
if not Enabled and (Parent <> nil) then RemoveFocus(False);
// ^^ False if form itself is being disabled!
if HandleAllocated and not (csDesigning in ComponentState) then
EnableWindow(WindowHandle, Enabled);
end;
procedure TWinControl.RemoveFocus(Removing: Boolean);
var
Form: TCustomForm;
begin
Form := GetParentForm(Self);
if Form <> nil then Form.DefocusControl(Self, Removing);
end
Where
procedure TCustomForm.DefocusControl(Control: TWinControl; Removing: Boolean);
begin
if Removing and Control.ContainsControl(FFocusedControl) then
FFocusedControl := Control.Parent;
if Control.ContainsControl(FActiveControl) then SetActiveControl(nil);
end;
This partially explains the above observed behaviour - focus moves to the parent control and the active control loses focus. It still doesn't explain why the 'EnableWindow` fails to kill focus to the button's child window. This does start to seem like a WinAPI problem...

Delphi: how to handle click on PageControl's empty space?

I'm using Delphi 7.
I want to react on click(left) on empty space of PageControl -- on area righter than the last tab shown. How can i do that?
You can handle the click at the parent control of the PageControl. F.i. if the PageControl is placed on a form, the form's 'MouseDown' events will be called for that specified region. The reason is that the PageControl returns HTTRANSPARENT for hit test messages for that region, so the mouse messages is directed to the control beneath it.
If that's not OK, you can change how WM_NCHITTEST is handled, for example by subclassing the control, or in a derived control:
type
TMyPageControl = class(TPageControl)
protected
procedure WMNCHitTest(var Message: TWMNCHitTest); message WM_NCHITTEST;
end;
procedure TMyPageControl.WMNCHitTest(var Message: TWMNCHitTest);
begin
inherited;
if Message.Result = HTTRANSPARENT then
Message.Result := HTCLIENT;
end;
then, the control's OnMouseDown event will be fired. Of course you could test for the region before modifying the message's return value, this example was only to show how it would work.

Delphi form without system menu but with close button

By default, a form having BorderStyle=bsSizeable has a system menu (on the left) and a close button ('X', on the right). I want to get rid of the system menu and keep the close button.
The BorderIcons property lets me remove the system menu (via biSystemmenu), but now the close button is gone too.
Is there a way to do this?
Using Delphi XE
PS: it should be possible as far as Windows is concerned: IE8's "InPrivate Filtering settings" window is sizeable, has a close button and has no system menu.
BorderStyle := bsSizeToolWin does what you want, with a slightly different appearance of the X button.
By "system menu" do you mean icon on the left of title bar? Or popup menu invoked via right click?
If it is icon that you want to remove - use this code:
const
WM_ResetIcon = WM_APP - 1;
type
TForm1 = class(TForm)
procedure FormShow(Sender: TObject);
protected
procedure WMResetIcon(var Message: TMessage); message WM_ResetIcon;
end;
implementation
procedure TForm1.FormShow(Sender: TObject);
begin
PostMessage(Handle, WM_ResetIcon, 0, 0);
end;
procedure TForm1.WMResetIcon(var Message: TMessage);
const
ICON_SMALL = 0;
ICON_BIG = 1;
begin
DestroyIcon(SendMessage(Handle, WM_SETICON, ICON_BIG, 0));
DestroyIcon(SendMessage(Handle, WM_SETICON, ICON_SMALL, 0));
end;
I don't think there is a way to do this without resorting to custom drawing your non-client area which is very difficult when glass is involved.
Consider this method.
procedure TMyForm.DeleteSystemMenu;
var
SystemMenu: HMenu;
begin
SystemMenu := GetSystemMenu(Handle, False);
DeleteMenu(SystemMenu, SC_CLOSE, MF_BYCOMMAND);
end;
Yes it succeeds in getting rid of the close item from the system menu, but it also results in the close button being disabled. So it would seem that you can't have one without the other.

TPageControl tab area OnMouseEnter OnMouseLeave events

I need to catch the "OnMouseEnter" and "0nMouseLeave" for a certain area of the TPageControl component. With that specific area I mean the whole "tab header" rectangle.
The problem is, that the page control doesn't catch the messages (I'm catching internal control messages CM_MOUSEENTER and CM_MOUSELEAVE) in the "empty" space.
The aim for me is to draw a small arrow in the right empty side when user hovers in the red framed area (and drawing is just piece of cake) and erase it when leaves this area. And I'm don't care about the overflow of the tabs (which causes to draw scrolling double button) - that will never happen.
Here is the working piece of code, but it's not the clear solution and I don't like it. There must be another (more clean) way to do it.
type
TPageControl = class(ComCtrls.TPageControl)
protected
procedure CMMouseLeave(var Message: TMessage); message CM_MOUSELEAVE;
procedure WMNCHitTest(var Message: TWMNCHitTest); message WM_NCHITTEST;
end;
procedure TPageControl.CMMouseLeave(var Message: TMessage);
begin
inherited;
Canvas.TextOut(Width - 130, 5, 'CMMouseLeave'); // display result
end;
procedure TPageControl.WMNCHitTest(var Message: TWMNCHitTest);
var TabHeaderRect: TRect;
begin
if Message.Result = 0 then // if Message.Result = HTNOWHERE ...
begin
TabHeaderRect := ClientRect;
TabHeaderRect.Bottom := Top + 21;
if PtInRect(TabHeaderRect, ScreenToClient(Point(Message.XPos, Message.YPos))) then
Canvas.TextOut(Width - 130, 5, 'WMNCHitTest '); // display result
Message.Result := HTCLIENT;
end
else
inherited;
end;
Obviously, the empty space does not belong to the control's client area and so the control doesn't get any mouse-related Windows messages for that area. You will have to use the form's mouse events. Or put the page control inside a panel (using alClient) and use the panel's mouse events.
If you need this more than once, you could create a new component that does exactly that (combine a panel and a page control to achieve the desired behaviour).
Are you sure you're handling OnMouseEnter/OnMouseLeave for the page control itself, and not for the TTabSheet instance that it contains?

Resources