How to change the User Agent string in DCEF3 - delphi

I've been looking to change the User Agent string in the Delphi Chromium Embedded Framework, but can"t seem to find a way.
Having looked through ceflib.pas, I see that it can be set, but there is no obvious call that I can make, such as:
Chromium.SetUserAgent('string');
or:
Chromium.Browser.useragent = 'string';
(Note: I am battling to interface with this component - at least to it's full potential - as there seems to be no decent documentation, if any.)

dont use the TChromium component, instead create your client at run time, and use the cefloadlib to customize it, see this example:
CefLoadLib('','this_is_my_user_agent','','','','',LOGSEVERITY_DISABLE,ANGLE_IN_PROCESS,0,0);
a full sample program can be found in **dcef-r306\dcef\demos\cefclient**
this is the full code of the sample program with a customized user agent(search for stackoverflow and you will find the changed code it):
{$IFDEF FPC}
{$MODE DELPHI}{$H+}
{$APPTYPE GUI}
{$ENDIF}
{$I cef.inc}
program cefclient;
uses
Classes,
Windows,
Messages,
SysUtils,
ceflib,
ceffilescheme in '..\filescheme\ceffilescheme.pas';
type
TCustomClient = class(TCefClientOwn)
private
FLifeSpan: ICefBase;
FLoad: ICefBase;
FDisplay: ICefBase;
protected
function GetLifeSpanHandler: ICefBase; override;
function GetLoadHandler: ICefBase; override;
function GetDisplayHandler: ICefBase; override;
public
constructor Create; override;
end;
TCustomLifeSpan = class(TCefLifeSpanHandlerOwn)
protected
procedure OnAfterCreated(const browser: ICefBrowser); override;
end;
TCustomLoad = class(TCefLoadHandlerOwn)
protected
procedure OnLoadStart(const browser: ICefBrowser; const frame: ICefFrame); override;
procedure OnLoadEnd(const browser: ICefBrowser; const frame: ICefFrame;
httpStatusCode: Integer); override;
end;
TCustomDisplay = class(TCefDisplayHandlerOwn)
protected
procedure OnAddressChange(const browser: ICefBrowser;
const frame: ICefFrame; const url: ustring); override;
procedure OnTitleChange(const browser: ICefBrowser; const title: ustring); override;
end;
TScheme = class(TCefSchemeHandlerOwn)
private
FResponse: TMemoryStream;
procedure Output(const str: ustring);
protected
function ProcessRequest(const Request: ICefRequest; var redirectUrl: ustring;
const callback: ICefSchemeHandlerCallback): Boolean; override;
procedure GetResponseHeaders(const response: ICefResponse; var responseLength: Int64); override;
function ReadResponse(DataOut: Pointer; BytesToRead: Integer;
var BytesRead: Integer; const callback: ICefSchemeHandlerCallback): Boolean; override;
public
constructor Create(SyncMainThread: Boolean;
const scheme: ustring; const browser: ICefBrowser; const request: ICefRequest); override;
destructor Destroy; override;
end;
TExtension = class(TCefv8HandlerOwn)
private
FTestParam: ustring;
protected
function Execute(const name: ustring; const obj: ICefv8Value;
const arguments: TCefv8ValueArray; var retval: ICefv8Value;
var exception: ustring): Boolean; override;
end;
type
{$IFDEF FPC}
TWindowProc = LongInt;
{$ELSE}
TWindowProc = Pointer;
WNDPROC = Pointer;
{$ENDIF}
var
Window : HWND;
handl: ICefBase = nil;
brows: ICefBrowser = nil;
browsrHwnd: HWND = INVALID_HANDLE_VALUE;
navigateto: ustring = 'http://www.google.com';
backWnd, forwardWnd, reloadWnd, stopWnd, editWnd: HWND;
editWndOldProc: TWindowProc;
isLoading, canGoBack, canGoForward: Boolean;
const
MAX_LOADSTRING = 100;
MAX_URL_LENGTH = 255;
BUTTON_WIDTH = 72;
URLBAR_HEIGHT = 24;
IDC_NAV_BACK = 200;
IDC_NAV_FORWARD = 201;
IDC_NAV_RELOAD = 202;
IDC_NAV_STOP = 203;
function CefWndProc(Wnd: HWND; message: UINT; wParam: Integer; lParam: Integer): Integer; stdcall;
var
ps: PAINTSTRUCT;
info: TCefWindowInfo;
rect: TRect;
hdwp: THandle;
x: Integer;
strPtr: array[0..MAX_URL_LENGTH-1] of WideChar;
strLen, urloffset: Integer;
begin
if Wnd = editWnd then
case message of
WM_CHAR:
if (wParam = VK_RETURN) then
begin
// When the user hits the enter key load the URL
FillChar(strPtr, SizeOf(strPtr), 0);
PDWORD(#strPtr)^ := MAX_URL_LENGTH;
strLen := SendMessageW(Wnd, EM_GETLINE, 0, Integer(#strPtr));
if (strLen > 0) then
begin
strPtr[strLen] := #0;
brows.MainFrame.LoadUrl(strPtr);
end;
Result := 0;
end else
Result := CallWindowProc(WNDPROC(editWndOldProc), Wnd, message, wParam, lParam);
else
Result := CallWindowProc(WNDPROC(editWndOldProc), Wnd, message, wParam, lParam);
end else
case message of
WM_PAINT:
begin
BeginPaint(Wnd, ps);
EndPaint(Wnd, ps);
result := 0;
end;
WM_CREATE:
begin
handl := TCustomClient.Create;
x := 0;
GetClientRect(Wnd, rect);
backWnd := CreateWindowW('BUTTON', 'Back',
WS_CHILD or WS_VISIBLE or BS_PUSHBUTTON
or WS_DISABLED, x, 0, BUTTON_WIDTH, URLBAR_HEIGHT,
Wnd, IDC_NAV_BACK, HInstance, nil);
Inc(x, BUTTON_WIDTH);
forwardWnd := CreateWindowW('BUTTON', 'Forward',
WS_CHILD or WS_VISIBLE or BS_PUSHBUTTON
or WS_DISABLED, x, 0, BUTTON_WIDTH,
URLBAR_HEIGHT, Wnd, IDC_NAV_FORWARD,
HInstance, nil);
Inc(x, BUTTON_WIDTH);
reloadWnd := CreateWindowW('BUTTON', 'Reload',
WS_CHILD or WS_VISIBLE or BS_PUSHBUTTON
or WS_DISABLED, x, 0, BUTTON_WIDTH,
URLBAR_HEIGHT, Wnd, IDC_NAV_RELOAD,
HInstance, nil);
Inc(x, BUTTON_WIDTH);
stopWnd := CreateWindowW('BUTTON', 'Stop',
WS_CHILD or WS_VISIBLE or BS_PUSHBUTTON
or WS_DISABLED, x, 0, BUTTON_WIDTH, URLBAR_HEIGHT,
Wnd, IDC_NAV_STOP, HInstance, nil);
Inc(x, BUTTON_WIDTH);
editWnd := CreateWindowW('EDIT', nil,
WS_CHILD or WS_VISIBLE or WS_BORDER or ES_LEFT or
ES_AUTOVSCROLL or ES_AUTOHSCROLL or WS_DISABLED,
x, 0, rect.right - BUTTON_WIDTH * 4,
URLBAR_HEIGHT, Wnd, 0, HInstance, nil);
// Assign the edit window's WNDPROC to this function so that we can
// capture the enter key
editWndOldProc := TWindowProc(GetWindowLong(editWnd, GWL_WNDPROC));
SetWindowLong(editWnd, GWL_WNDPROC, LongInt(#CefWndProc));
FillChar(info, SizeOf(info), 0);
Inc(rect.top, URLBAR_HEIGHT);
info.Style := WS_CHILD or WS_VISIBLE or WS_CLIPCHILDREN or WS_CLIPSIBLINGS or WS_TABSTOP;
info.WndParent := Wnd;
info.x := rect.left;
info.y := rect.top;
info.Width := rect.right - rect.left;
info.Height := rect.bottom - rect.top;
CefBrowserCreate(#info, handl.Wrap, navigateto, nil);
isLoading := False;
canGoBack := False;
canGoForward := False;
SetTimer(Wnd, 1, 100, nil);
result := 0;
end;
WM_TIMER:
begin
// Update the status of child windows
EnableWindow(editWnd, True);
EnableWindow(backWnd, canGoBack);
EnableWindow(forwardWnd, canGoForward);
EnableWindow(reloadWnd, not isLoading);
EnableWindow(stopWnd, isLoading);
Result := 0;
end;
WM_COMMAND:
case LOWORD(wParam) of
IDC_NAV_BACK:
begin
brows.GoBack;
Result := 0;
end;
IDC_NAV_FORWARD:
begin
brows.GoForward;
Result := 0;
end;
IDC_NAV_RELOAD:
begin
brows.Reload;
Result := 0;
end;
IDC_NAV_STOP:
begin
brows.StopLoad;
Result := 0;
end;
else
result := DefWindowProc(Wnd, message, wParam, lParam);
end;
WM_DESTROY:
begin
brows := nil;
PostQuitMessage(0);
result := 0;
end;
WM_SETFOCUS:
begin
if browsrHwnd <> INVALID_HANDLE_VALUE then
PostMessage(browsrHwnd, WM_SETFOCUS, wParam, 0);
Result := 0;
end;
WM_SIZE:
begin
if(browsrHwnd <> INVALID_HANDLE_VALUE) then
begin
// Resize the browser window and address bar to match the new frame
// window size
GetClientRect(Wnd, rect);
Inc(rect.top, URLBAR_HEIGHT);
urloffset := rect.left + BUTTON_WIDTH * 4;
hdwp := BeginDeferWindowPos(1);
hdwp := DeferWindowPos(hdwp, editWnd, 0, urloffset, 0, rect.right - urloffset, URLBAR_HEIGHT, SWP_NOZORDER);
hdwp := DeferWindowPos(hdwp, browsrHwnd, 0, rect.left, rect.top,
rect.right - rect.left, rect.bottom - rect.top, SWP_NOZORDER);
EndDeferWindowPos(hdwp);
end;
result := DefWindowProc(Wnd, message, wParam, lParam);
end;
WM_CLOSE:
begin
if brows <> nil then
brows.ParentWindowWillClose;
result := DefWindowProc(Wnd, message, wParam, lParam);
end
else
result := DefWindowProc(Wnd, message, wParam, lParam);
end;
end;
{ TCustomClient }
constructor TCustomClient.Create;
begin
inherited;
FLifeSpan := TCustomLifeSpan.Create;
FLoad := TCustomLoad.Create;
FDisplay := TCustomDisplay.Create;
end;
function TCustomClient.GetDisplayHandler: ICefBase;
begin
Result := FDisplay;
end;
function TCustomClient.GetLifeSpanHandler: ICefBase;
begin
Result := FLifeSpan;
end;
function TCustomClient.GetLoadHandler: ICefBase;
begin
Result := FLoad;
end;
{ TCustomLifeSpan }
procedure TCustomLifeSpan.OnAfterCreated(const browser: ICefBrowser);
begin
if not browser.IsPopup then
begin
// get the first browser
brows := browser;
browsrHwnd := brows.GetWindowHandle;
end;
end;
{ TCustomLoad }
procedure TCustomLoad.OnLoadEnd(const browser: ICefBrowser;
const frame: ICefFrame; httpStatusCode: Integer);
begin
if browser.GetWindowHandle = browsrHwnd then
isLoading := False;
end;
procedure TCustomLoad.OnLoadStart(const browser: ICefBrowser;
const frame: ICefFrame);
begin
if browser.GetWindowHandle = browsrHwnd then
begin
isLoading := True;
canGoBack := browser.CanGoBack;
canGoForward := browser.CanGoForward;
end;
end;
{ TCustomDisplay }
procedure TCustomDisplay.OnAddressChange(const browser: ICefBrowser;
const frame: ICefFrame; const url: ustring);
begin
if (browser.GetWindowHandle = browsrHwnd) and frame.IsMain then
SetWindowTextW(editWnd, PWideChar(url));
end;
procedure TCustomDisplay.OnTitleChange(const browser: ICefBrowser;
const title: ustring);
begin
if browser.GetWindowHandle = browsrHwnd then
SetWindowTextW(Window, PWideChar(title));
end;
{ TScheme }
constructor TScheme.Create(SyncMainThread: Boolean;
const scheme: ustring; const browser: ICefBrowser; const request: ICefRequest);
begin
inherited;
FResponse := TMemoryStream.Create;
end;
destructor TScheme.Destroy;
begin
FResponse.Free;
inherited;
end;
function TScheme.ProcessRequest(const Request: ICefRequest; var redirectUrl: ustring;
const callback: ICefSchemeHandlerCallback): Boolean;
begin
OutPut('<html>');
OutPut(' <body>ClientV8ExtensionHandler says:<br><pre>');
OutPut('<script language="javascript">');
OutPut(' cef.test.test_param =''Assign and retrieve a value succeeded the first time.'';');
OutPut(' document.writeln(cef.test.test_param);');
OutPut(' cef.test.test_param = ''Assign and retrieve a value succeeded the second time.'';');
OutPut(' document.writeln(cef.test.test_param);');
OutPut(' var obj = cef.test.test_object();');
OutPut(' document.writeln(obj.param);');
OutPut(' document.writeln(obj.GetMessage());');
OutPut('</script>');
OutPut('</pre></body>');
OutPut('</html>');
FResponse.Seek(0, soFromBeginning);
callback.HeadersAvailable;
callback.BytesAvailable;
Result := True;
end;
procedure TScheme.GetResponseHeaders(const response: ICefResponse;
var responseLength: Int64);
begin
response.Status := 200;
response.StatusText := 'OK';
response.MimeType := 'text/html';
ResponseLength := FResponse.Size;
end;
function TScheme.ReadResponse(DataOut: Pointer; BytesToRead: Integer;
var BytesRead: Integer; const callback: ICefSchemeHandlerCallback): Boolean;
begin
BytesRead := FResponse.Read(DataOut^, BytesToRead);
Result := True;
end;
procedure TScheme.Output(const str: ustring);
var
u: UTF8String;
begin
{$IFDEF UNICODE}
u := UTF8String(str);
{$ELSE}
u := UTF8Encode(str);
{$ENDIF}
FResponse.Write(PAnsiChar(u)^, Length(u));
end;
function TExtension.Execute(const name: ustring; const obj: ICefv8Value;
const arguments: TCefv8ValueArray; var retval: ICefv8Value;
var exception: ustring): Boolean;
begin
if(name = 'SetTestParam') then
begin
// Handle the SetTestParam native function by saving the string argument
// into the local member.
if (Length(arguments) <> 1) or (not arguments[0].IsString) then
begin
Result := false;
Exit;
end;
FTestParam := arguments[0].GetStringValue;
Result := true;
end
else if(name = 'GetTestParam') then
begin
// Handle the GetTestParam native function by returning the local member
// value.
retval := TCefv8ValueRef.CreateString(Ftestparam);
Result := true;
end
else if (name = 'GetTestObject') then
begin
// Handle the GetTestObject native function by creating and returning a
// new V8 object.
retval := TCefv8ValueRef.CreateObject(nil);
// Add a string parameter to the new V8 object.
retval.SetValueByKey('param', TCefv8ValueRef.CreateString(
'Retrieving a parameter on a native object succeeded.'));
// Add a function to the new V8 object.
retval.SetValueByKey('GetMessage',
TCefv8ValueRef.CreateFunction('GetMessage', Self));
Result := true;
end
else if(name = 'GetMessage') then
begin
// Handle the GetMessage object function by returning a string.
retval := TCefv8ValueRef.CreateString(
'Calling a function on a native object succeeded.');
Result := true;
end else
Result := false;
end;
const
code =
'var cef;'+
'if (!cef)'+
' cef = {};'+
'if (!cef.test)'+
' cef.test = {};'+
'(function() {'+
' cef.test.__defineGetter__(''test_param'', function() {'+
' native function GetTestParam();'+
' return GetTestParam();'+
' });'+
' cef.test.__defineSetter__(''test_param'', function(b) {'+
' native function SetTestParam();'+
' if(b) SetTestParam(b);'+
' });'+
' cef.test.test_object = function() {'+
' native function GetTestObject();'+
' return GetTestObject();'+
' };'+
'})();';
var
{$IFDEF CEF_MULTI_THREADED_MESSAGE_LOOP}
Msg : TMsg;
{$ENDIF}
wndClass : TWndClass;
begin
CefCache := 'cache';
CefLoadLib('','stackoverflow','','','','',LOGSEVERITY_DISABLE,ANGLE_IN_PROCESS,0,0);
CefRegisterCustomScheme('client', True, False, False);
CefRegisterCustomScheme('file', True, False, False);
CefRegisterSchemeHandlerFactory('client', 'test', False, TScheme);
CefRegisterSchemeHandlerFactory('file', '', False, TFileScheme);
CefRegisterExtension('v8/test', code, TExtension.Create as ICefV8Handler);
//navigateto := 'client://test/';
//navigateto := 'file://c:\';
try
wndClass.style := CS_HREDRAW or CS_VREDRAW;
wndClass.lpfnWndProc := #CefWndProc;
wndClass.cbClsExtra := 0;
wndClass.cbWndExtra := 0;
wndClass.hInstance := hInstance;
wndClass.hIcon := LoadIcon(0, IDI_APPLICATION);
wndClass.hCursor := LoadCursor(0, IDC_ARROW);
wndClass.hbrBackground := 0;
wndClass.lpszMenuName := nil;
wndClass.lpszClassName := 'chromium';
RegisterClass(wndClass);
Window := CreateWindow(
'chromium', // window class name
'Chromium browser', // window caption
WS_OVERLAPPEDWINDOW or WS_CLIPCHILDREN, // window style
Integer(CW_USEDEFAULT), // initial x position
Integer(CW_USEDEFAULT), // initial y position
Integer(CW_USEDEFAULT), // initial x size
Integer(CW_USEDEFAULT), // initial y size
0, // parent window handle
0, // window menu handle
hInstance, // program instance handle
nil); // creation parameters
ShowWindow(Window, SW_SHOW);
UpdateWindow(Window);
{$IFNDEF CEF_MULTI_THREADED_MESSAGE_LOOP}
CefRunMessageLoop;
{$ELSE}
while(GetMessageW(msg, 0, 0, 0)) do
begin
TranslateMessage(msg);
DispatchMessageW(msg);
end;
{$ENDIF}
finally
handl := nil;
end;
end.
if you still want to use the TChromium Component then you should see this article:
chromiumembedded/issues
they have made a patch for this issue, but i think you need to apply the patch and recompile the lib.
use this link to test the result :
whatismyuseragent

Another way is to define CefUserAgent in ceflib.pas function CefLoadLibDefault: Boolean;
function CefLoadLibDefault: Boolean;
begin
CefUserAgent:='Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)'; //addition for example
if LibHandle = 0 then
Result := CefLoadLib(CefCache, CefUserAgent, CefProductVersion, CefLocale, CefLogFile,
CefBrowserSubprocessPath, CefLogSeverity,
CefJavaScriptFlags, CefResourcesDirPath, CefLocalesDirPath, CefSingleProcess,
CefCommandLineArgsDisabled, CefPackLoadingDisabled, CefRemoteDebuggingPort,
CefReleaseDCheck, CefUncaughtExceptionStackSize, CefContextSafetyImplementation) else
Result := True;
end;

Related

Delphi RichEdit - add multiple CFE_LINK effect via CHARFORMAT2

I have made a code, that inserts CFE_LINK into RichEdit text, but it works only for last inserted text. All previous insertions of Links are Undone.
I want to insert multiple Link-texts, but I cant figure out how to do that.
Here is a working code (with no errors):
unit uRichEditExtended;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, RichEdit, WinApi.ShellApi, Vcl.Controls, Vcl.ComCtrls,
Generics.Collections;
type
TZ_RichEditClickEvent = reference to procedure(const ALinkText: string);
TZ_RichEditLink = class
IsDefaultEvent: boolean;
Text: string;
OnLinkClickEvent: TZ_RichEditClickEvent;
end;
TZ_RichEditLinks = TList<TZ_RichEditLink>;
TRichEditExtended = class
protected
class var FInstance: TRichEditExtended;
private
FPrevRichEditWndProc: TWndMethod;
FRichEdit: TRichEdit;
FRichEditLinks: TZ_RichEditLinks;
procedure InsertLinkText(const LinkText: string; SelStart: integer = -1);
procedure SetRichEditMasks;
procedure RichEditWndProc(var Message: TMessage);
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
public
class function This: TRichEditExtended;
class procedure ApplyRichEdit(ARichEdit: TRichEdit); // -1 - inserts to the end of text, otherwise into a position indicated by SelStart
class function AddLinkText(AText: string; AOnLinkClickEvent: TZ_RichEditClickEvent; SelStart: integer = -1): integer;
class function AddLinkTextWithDefaultEvent(AText: string; SelStart: integer = -1): integer;
class procedure AddDefaultLinkTextEvent(AOnLinkClickEvent: TZ_RichEditClickEvent);
end;
implementation
{ TRichEditExtended }
uses StrUtils;
class procedure TRichEditExtended.AddDefaultLinkTextEvent(AOnLinkClickEvent: TZ_RichEditClickEvent);
var
REL: TZ_RichEditLink;
begin
if (This.FRichEditLinks.Count > 0) and This.FRichEditLinks[0].IsDefaultEvent then
This.FRichEditLinks[0].OnLinkClickEvent := AOnLinkClickEvent
else
begin
REL := TZ_RichEditLink.Create;
REL.IsDefaultEvent := true;
REL.Text := '';
REL.OnLinkClickEvent := AOnLinkClickEvent;
This.FRichEditLinks.Insert(0, REL);
REL := nil;
end;
end;
class function TRichEditExtended.AddLinkText(AText: string; AOnLinkClickEvent: TZ_RichEditClickEvent; SelStart: integer = -1): integer;
var REL: TZ_RichEditLink;
begin
REL := TZ_RichEditLink.Create;
REL.IsDefaultEvent := false;
REL.Text := AText;
REL.OnLinkClickEvent := AOnLinkClickEvent;
Result := This.FRichEditLinks.Add(REL);
This.InsertLinkText(AText, SelStart);
REL := nil;
end;
class function TRichEditExtended.AddLinkTextWithDefaultEvent(AText: string; SelStart: integer): integer;
begin
This.AddLinkText(AText, nil, SelStart);
end;
procedure TRichEditExtended.AfterConstruction;
begin
inherited;
FRichEdit := nil;
FRichEditLinks := TZ_RichEditLinks.Create;
end;
class procedure TRichEditExtended.ApplyRichEdit(ARichEdit: TRichEdit);
begin
This.FRichEdit := ARichEdit;
This.FPrevRichEditWndProc := This.FRichEdit.WindowProc;
This.FRichEdit.WindowProc := This.RichEditWndProc;
This.FRichEditLinks.Clear;
This.SetRichEditMasks;
end;
procedure TRichEditExtended.BeforeDestruction;
begin
if Assigned(FRichEdit) then
FRichEdit.WindowProc := FPrevRichEditWndProc;
FRichEdit := nil;
FRichEditLinks.Clear;
FRichEditLinks.Free;
inherited;
end;
procedure TRichEditExtended.InsertLinkText(const LinkText: string; SelStart: integer = -1);
var
Fmt: CHARFORMAT2;
begin
if SelStart = -1 then
begin
SelStart := FRichEdit.Lines.Text.Length - 1;
FRichEdit.Text := FRichEdit.Text + LinkText;
dec(SelStart,2 * (FRichEdit.Lines.Text.CountChar(#$D) - 1));
end
else
begin
FRichEdit.SelStart := SelStart;
FRichEdit.SelText := LinkText;
end;
FRichEdit.SelStart := SelStart;
FRichEdit.SelLength := Length(LinkText);
FillChar(Fmt, SizeOf(Fmt), 0);
Fmt.cbSize := SizeOf(Fmt);
Fmt.dwMask := CFM_LINK;
Fmt.dwEffects := CFE_LINK;
SendMessage(FRichEdit.Handle, EM_SETCHARFORMAT, SCF_SELECTION, LPARAM(#Fmt));
FRichEdit.SelStart := SelStart + Length(LinkText);
FRichEdit.SelLength := 0;
end;
procedure TRichEditExtended.RichEditWndProc(var Message: TMessage);
type
PENLINK = ^ENLINK;
var
tr: TEXTRANGE;
str: string;
p: PENLINK;
i: integer;
begin
FPrevRichEditWndProc(Message);
case Message.Msg of
CN_NOTIFY: begin
if TWMNotify(Message).NMHdr.code = EN_LINK then
begin
P := PENLINK(Message.LParam);
if p.msg = WM_LBUTTONDOWN then
begin
SetLength(str, p.chrg.cpMax - p.chrg.cpMin);
tr.chrg := p.chrg;
tr.lpstrText := PChar(str);
SendMessage(FRichEdit.Handle, EM_GETTEXTRANGE, 0, LPARAM(#tr));
for I := 0 to FRichEditLinks.Count - 1 do
if str.ToUpper.Equals(FRichEditLinks[I].Text.ToUpper) then
begin
if not Assigned(FRichEditLinks[I].OnLinkClickEvent) then
begin
if not FRichEditLinks[0].IsDefaultEvent then
raise Exception.Create('No default event is set.')
else
FRichEditLinks[0].OnLinkClickEvent(str)
end
else
FRichEditLinks[I].OnLinkClickEvent(str);
exit;
end;
end;
end;
end;
CM_RECREATEWND: begin
SetRichEditMasks;
end;
end;
end;
procedure TRichEditExtended.SetRichEditMasks;
var
Mask: DWORD;
begin
Mask := SendMessage(FRichEdit.Handle, EM_GETEVENTMASK, 0, 0);
SendMessage(FRichEdit.Handle, EM_SETEVENTMASK, 0, Mask or ENM_LINK);
SendMessage(FRichEdit.Handle, EM_AUTOURLDETECT, 1, 0);
end;
class function TRichEditExtended.This: TRichEditExtended;
begin
if not Assigned(TRichEditExtended.FInstance) then
TRichEditExtended.FInstance := TRichEditExtended.Create;
Result := TRichEditExtended.FInstance;
end;
{ TRichEditExList }
initialization
finalization
if Assigned(TRichEditExtended.FInstance) then
TRichEditExtended.FInstance.Free;
end.
To run this code you should create a new Application, add TRichEdit on the Form and type the following in a FormCreate method:
TRichEditExtended.ApplyRichEdit(ed1);
TRichEditExtended.AddDefaultLinkTextEvent(procedure (const T: String)begin showmessage(T); end);
TRichEditExtended.AddLinkTextWithDefaultEvent('Link');
ed1.Text := ed1.Text + '1231232 ';
TRichEditExtended.AddLinkTextWithDefaultEvent('Link2');
InsertLinkText() is replacing FRichEdit.Text with a completely new string when inserting a link with SelStart=-1, thus losing all previous text and formatting.
Use FRichEdit.GetTextLen() instead of FRichEdit.Lines.Text.Length to get the length of the existing text. And regardless of the input SelStart, always use the FRichEdit.SelStart|SelLength|SelText properties to add the new link into FRichEdit, preserving all existing text and formatting.
procedure TRichEditExtended.InsertLinkText(const LinkText: string; SelStart: integer = -1);
var
Fmt: CHARFORMAT2;
//Range: CHARRANGE;
begin
if SelStart = -1 then SelStart := FRichEdit.GetTextLen;
FRichEdit.SelStart := SelStart;
FRichEdit.SelLength := 0;
{ or:
Range.cpMin := SelStart;
Range.cpMax := SelStart;
SendMessage(FRichEdit.Handle, EM_EXSETSEL, 0, LPARAM(#Range));
}
FRichEdit.SelText := LinkText;
FRichEdit.SelStart := SelStart;
FRichEdit.SelLength := Length(LinkText);
{ or:
Range.cpMin := SelStart;
Range.cpMax := SelStart + Length(LinkText);
SendMessage(FRichEdit.Handle, EM_EXSETSEL, 0, LPARAM(#Range));
}
FillChar(Fmt, SizeOf(Fmt), 0);
Fmt.cbSize := SizeOf(Fmt);
Fmt.dwMask := CFM_LINK;
Fmt.dwEffects := CFE_LINK;
SendMessage(FRichEdit.Handle, EM_SETCHARFORMAT, SCF_SELECTION, LPARAM(#Fmt));
FRichEdit.SelStart := SelStart + Length(LinkText);
FRichEdit.SelLength := 0;
{ or:
Range.cpMin := SelStart + Length(LinkText);
Range.cpMax := Range.cpMax;
SendMessage(FRichEdit.Handle, EM_EXSETSEL, 0, LPARAM(#Range));
}
end;
As suggested by #KenWhite I will post my reseach upon the topic with edits about correct text highlighting due to issue of CRLF symbols in the text. As I found out line breaks are counted as one character for SelStart and GetTextLen, so you need to find the line where you are going to place a text and count all breaks before it and substract it from the desired SelStart position. For that purpose a function GetReilableSelStart is.
unit uRichEditExtended;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, RichEdit, WinApi.ShellApi, Vcl.Controls, Vcl.ComCtrls,
Generics.Collections, Vcl.Graphics;
type
TZ_RichEditClickEvent = reference to procedure(const ALinkText: string; LinkClickAccepted: boolean; var OutData: string);
TZ_RichEditLink = class
IsDefaultEvent: boolean;
Text: string;
OnLinkClickEvent: TZ_RichEditClickEvent;
end;
TZ_RichEditLinks = TList<TZ_RichEditLink>;
TZ_RichEditInsertOptions = set of (rioAppendBeforeCRLF);
TRichEditExtended = class
protected
class var FInstance: TRichEditExtended;
procedure SetRichEditMasks;
procedure RichEditWndProc(var Message: TMessage);
function GetReilableSelStart(SelStart: integer; InsertOptions: TZ_RichEditInsertOptions = [rioAppendBeforeCRLF]): integer;
private
FLastPressedLinkText: string;
FLinkClickAccepted: boolean;
FPrevRichEditWndProc: TWndMethod;
FRichEdit: TRichEdit;
FRichEditLinks: TZ_RichEditLinks;
function InsertText(const AText: string; const SelStart: integer = -1; InsertOptions: TZ_RichEditInsertOptions = [rioAppendBeforeCRLF]; Font: TFont = nil;
IsLink: boolean = false): integer;
procedure InsertLinkText(const LinkText: string; SelStart: integer = -1; InsertOptions: TZ_RichEditInsertOptions = [rioAppendBeforeCRLF]);
procedure AddText(const AText: string; AddCRLF: boolean; Font: TFont = nil);
procedure AddFmtText(const AText: string; FontSizeDelta: integer; FontStyle: TFontStyles; FontColor: TColor; AddCRLF: boolean);
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
public
class function This: TRichEditExtended;
class procedure ApplyRichEdit(ARichEdit: TRichEdit);
class procedure AppendText(AText: string);
class procedure AppendTextLine(AText: string);
class procedure AppendFmtText(AText: string; FontSizeDelta: integer; FontStyle: TFontStyles; FontColor: TColor);
class procedure AppendFmtTextLine(AText: string; FontSizeDelta: integer; FontStyle: TFontStyles; FontColor: TColor);
class function AppendLinkText(AText: string; AOnLinkClickEvent: TZ_RichEditClickEvent; SelStart: integer = -1; InsertOptions: TZ_RichEditInsertOptions = [rioAppendBeforeCRLF]): integer;
class function AppendLinkTextWithDefaultEvent(AText: string; SelStart: integer = -1; InsertOptions: TZ_RichEditInsertOptions = [rioAppendBeforeCRLF]): integer;
class procedure AddDefaultLinkClickEvent(AOnLinkClickEvent: TZ_RichEditClickEvent);
class function LastLinkText: string;
class procedure PerformDefaultLinkClickEvent(const LinkText: string; CanOpen: boolean; var FullFilePath: string);
class procedure SetDefaultLinkClickReaction(ALinkClickAccepted: boolean);
end;
implementation
{ TRichEditExtended }
uses StrUtils, Math;
class procedure TRichEditExtended.AddDefaultLinkClickEvent(AOnLinkClickEvent: TZ_RichEditClickEvent);
var
REL: TZ_RichEditLink;
begin
if (This.FRichEditLinks.Count > 0) and This.FRichEditLinks[0].IsDefaultEvent then
This.FRichEditLinks[0].OnLinkClickEvent := AOnLinkClickEvent
else
begin
REL := TZ_RichEditLink.Create;
REL.IsDefaultEvent := true;
REL.Text := '';
REL.OnLinkClickEvent := AOnLinkClickEvent;
This.FRichEditLinks.Insert(0, REL);
REL := nil;
end;
end;
procedure TRichEditExtended.AddFmtText(const AText: string; FontSizeDelta: integer; FontStyle: TFontStyles; FontColor: TColor; AddCRLF: boolean);
var Font: TFont;
begin
try
Font := TFont.Create;
Font.Size := This.FRichEdit.Font.Size + FontSizeDelta;
Font.Style := FontStyle;
Font.Color := FontColor;
Font.Name := This.FRichEdit.Font.Name;
This.AddText(AText, AddCRLF, Font);
finally
FreeAndNil(Font);
end;
end;
class function TRichEditExtended.AppendLinkText(AText: string; AOnLinkClickEvent: TZ_RichEditClickEvent; SelStart: integer = -1;
InsertOptions: TZ_RichEditInsertOptions = [rioAppendBeforeCRLF]): integer;
var REL: TZ_RichEditLink;
begin
REL := TZ_RichEditLink.Create;
REL.IsDefaultEvent := false;
REL.Text := AText;
REL.OnLinkClickEvent := AOnLinkClickEvent;
Result := This.FRichEditLinks.Add(REL);
This.InsertLinkText(AText, SelStart, InsertOptions);
REL := nil;
end;
class function TRichEditExtended.AppendLinkTextWithDefaultEvent(AText: string; SelStart: integer = -1; InsertOptions: TZ_RichEditInsertOptions = [rioAppendBeforeCRLF]): integer;
begin
This.AppendLinkText(AText, nil, SelStart, InsertOptions);
end;
procedure TRichEditExtended.AfterConstruction;
begin
inherited;
FRichEdit := nil;
FRichEditLinks := TZ_RichEditLinks.Create;
FLinkClickAccepted := false;
end;
procedure TRichEditExtended.AddText(const AText: string; AddCRLF: boolean; Font: TFont);
begin
if AddCRLF then
InsertText(Format('%s'#13#10,[AText]), -1, [rioAppendBeforeCRLF], Font)
else
InsertText(AText, -1, [rioAppendBeforeCRLF], Font);
end;
class procedure TRichEditExtended.AppendFmtText(AText: string; FontSizeDelta: integer; FontStyle: TFontStyles; FontColor: TColor);
begin
This.AddFmtText(AText, FontSizeDelta, FontStyle, FontColor, false);
end;
class procedure TRichEditExtended.AppendFmtTextLine(AText: string; FontSizeDelta: integer; FontStyle: TFontStyles; FontColor: TColor);
begin
This.AddFmtText(AText, FontSizeDelta, FontStyle, FontColor, true);
end;
class procedure TRichEditExtended.AppendText(AText: string);
begin
This.AddText(AText, false);
end;
class procedure TRichEditExtended.AppendTextLine(AText: string);
begin
This.AddText(AText, true);
end;
class procedure TRichEditExtended.ApplyRichEdit(ARichEdit: TRichEdit);
begin
This.FRichEdit := ARichEdit;
This.FPrevRichEditWndProc := This.FRichEdit.WindowProc;
This.FRichEdit.WindowProc := This.RichEditWndProc;
This.FRichEditLinks.Clear;
This.SetRichEditMasks;
end;
procedure TRichEditExtended.BeforeDestruction;
begin
if Assigned(FRichEdit) then
FRichEdit.WindowProc := FPrevRichEditWndProc;
FRichEdit := nil;
FRichEditLinks.Clear;
FRichEditLinks.Free;
inherited;
end;
function TRichEditExtended.GetReilableSelStart(SelStart: integer; InsertOptions: TZ_RichEditInsertOptions = [rioAppendBeforeCRLF]): integer;
var
LineNo, LinesCount: integer;
begin
LinesCount := FRichEdit.Lines.Count;
if SelStart = -1 then
begin
Result := Max(FRichEdit.GetTextLen - Max((LinesCount - ord(not String(FRichEdit.Text).EndsWith(#$D#$A))),0), 0);
end
else
begin
LineNo := FRichEdit.Perform(EM_LINEFROMCHAR, SelStart, 0);
Result := Max(SelStart - (Max(LineNo - ord(rioAppendBeforeCRLF in InsertOptions) * ord(FRichEdit.Lines[LineNo].EndsWith(#$D#$A)),0)), 0);
end;
end;
procedure TRichEditExtended.InsertLinkText(const LinkText: string; SelStart: integer = -1; InsertOptions: TZ_RichEditInsertOptions = [rioAppendBeforeCRLF]);
var
Fmt: CHARFORMAT2;
begin
SelStart := InsertText(LinkText, SelStart, InsertOptions, nil, true);
FillChar(Fmt, SizeOf(Fmt), 0);
Fmt.cbSize := SizeOf(Fmt);
Fmt.dwMask := CFM_LINK;
Fmt.dwEffects := CFE_LINK;
SendMessage(FRichEdit.Handle, EM_SETCHARFORMAT, SCF_SELECTION, LPARAM(#Fmt));
FRichEdit.SelStart := FRichEdit.GetTextLen;
FRichEdit.SelLength := 0;
end;
function TRichEditExtended.InsertText(const AText: string; const SelStart: integer = -1; InsertOptions: TZ_RichEditInsertOptions = [rioAppendBeforeCRLF];
Font: TFont = nil; IsLink: boolean = false): integer;
var
Fmt: CHARFORMAT2;
begin
Result := GetReilableSelStart(SelStart, InsertOptions);
FRichEdit.SelStart := Result;
FRichEdit.SelText := Format('%s%s',[AText, DupeString(#32,ord(IsLink))]);
FRichEdit.SelStart := Result;
FRichEdit.SelLength := Length(AText);
FRichEdit.SelAttributes.Color := FRichEdit.DefAttributes.Color;
FRichEdit.SelAttributes.Name := FRichEdit.DefAttributes.Name;
FRichEdit.SelAttributes.Size := FRichEdit.DefAttributes.Size;
FRichEdit.SelAttributes.Style := FRichEdit.DefAttributes.Style;
if Assigned(Font) then
begin
FRichEdit.SelAttributes.Color := Font.Color;
FRichEdit.SelAttributes.Name := Font.Name;
FRichEdit.SelAttributes.Size := Font.Size;
FRichEdit.SelAttributes.Style := Font.Style;
FRichEdit.SelStart := FRichEdit.GetTextLen;
FRichEdit.SelLength := 0;
end;
end;
class function TRichEditExtended.LastLinkText: string;
begin
Result := This.FLastPressedLinkText;
end;
class procedure TRichEditExtended.PerformDefaultLinkClickEvent(const LinkText: string; CanOpen: boolean; var FullFilePath: string);
begin
if (This.FRichEditLinks.Count = 0) or not This.FRichEditLinks[0].IsDefaultEvent then
raise Exception.Create('A default action should be set.');
This.FRichEditLinks[0].OnLinkClickEvent(LinkText, CanOpen, FullFilePath);
end;
procedure TRichEditExtended.RichEditWndProc(var Message: TMessage);
type
PENLINK = ^ENLINK;
var
tr: TEXTRANGE;
p: PENLINK;
i: integer;
OutDat: string;
function GetLinkText: string;
begin
SetLength(Result, p.chrg.cpMax - p.chrg.cpMin);
tr.chrg := p.chrg;
tr.lpstrText := PChar(Result);
SendMessage(FRichEdit.Handle, EM_GETTEXTRANGE, 0, LPARAM(#tr));
end;
begin
FPrevRichEditWndProc(Message);
case Message.Msg of
CN_NOTIFY: begin
if TWMNotify(Message).NMHdr.code = EN_LINK then
begin
P := PENLINK(Message.LParam);
case p.msg of
WM_LBUTTONDOWN:
begin
FLastPressedLinkText := GetLinkText;
for I := 0 to FRichEditLinks.Count - 1 do
if FLastPressedLinkText.ToUpper.Equals(FRichEditLinks[I].Text.ToUpper) then
begin
if not Assigned(FRichEditLinks[I].OnLinkClickEvent) then
begin
if not FRichEditLinks[0].IsDefaultEvent then
raise Exception.Create('A default action should be set.')
else
FRichEditLinks[0].OnLinkClickEvent(FLastPressedLinkText, FLinkClickAccepted, OutData)
end
else
FRichEditLinks[I].OnLinkClickEvent(FLastPressedLinkText, FLinkClickAccepted, OutData);
exit;
end;
end;
WM_RBUTTONDOWN:
begin
FLastPressedLinkText := GetLinkText;
if Assigned(FRichEdit.PopupMenu) then
begin
FRichEdit.PopupMenu.Popup(Mouse.CursorPos.X, Mouse.CursorPos.Y);
end;
end;
end;
end;
end;
CM_RECREATEWND: begin
SetRichEditMasks;
end;
end;
end;
class procedure TRichEditExtended.SetDefaultLinkClickReaction(ALinkClickAccepted: boolean);
begin
This.FLinkClickAccepted := ALinkClickAccepted;
end;
procedure TRichEditExtended.SetRichEditMasks;
var
Mask: DWORD;
begin
Mask := SendMessage(FRichEdit.Handle, EM_GETEVENTMASK, 0, 0);
SendMessage(FRichEdit.Handle, EM_SETEVENTMASK, 0, Mask or ENM_LINK);
SendMessage(FRichEdit.Handle, EM_AUTOURLDETECT, 1, 0);
end;
class function TRichEditExtended.This: TRichEditExtended;
begin
if not Assigned(TRichEditExtended.FInstance) then
TRichEditExtended.FInstance := TRichEditExtended.Create;
Result := TRichEditExtended.FInstance;
end;
{ TRichEditExList }
initialization
finalization
if Assigned(TRichEditExtended.FInstance) then
TRichEditExtended.FInstance.Free;
end.
Any suggestions are appreciated.
How to use:
TRichEditExtended.ApplyRichEdit(edMessages);
TRichEditExtended.SetDefaultLinkClickReaction(true); //Link click accepted by default or not
TRichEditExtended.AddDefaultLinkClickEvent(
procedure (const LinkText: string; LinkClickAccepted: boolean; var OutData: string)
begin
if LinkClickAccepted then
DoSomething;
DoAnythingTo(OutData); // if you call somewhere after TRichEditExtended.PerformDefaultLinkClickEvent then you get the OutData there
end
);

Caption Buttons not respond to mouse clicks after Non - Client painting with DWM in Delphi

I am painting my Application's non-client area with the help of Desktop Window Manager, adding a new button for testing purposes.
After compiling, my custom button is clickable, but the default caption buttons (Minimize, Maximize and Close) do nothing when I hover over them or click on them.
The repainted Title Bar responds to dragging and double-clicks. The Form
maximizes when I double-click the Title Bar as default. And the Close button responds to the very corner of it near Form's Right Border.
I have written my painting procedure as described in this post.
The new codes I added:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, ImgList, Buttons;
type
TForm1 = class(TForm)
ImageList1: TImageList;
SpeedButton1: TSpeedButton;
function GetSysIconRect: TRect;
procedure PaintWindow(DC: HDC);
procedure InvalidateTitleBar;
procedure FormCreate(Sender: TObject);
procedure WndProc(var Message: TMessage);
procedure FormPaint(Sender: TObject);
procedure SpeedButton1Click(Sender: TObject);
protected
procedure WMNCCalcSize(var Message: TWMNCCalcSize); message WM_NCCALCSIZE;
procedure WMNCHitTest(var Message: TWMNCHitTest); message WM_NCHITTEST;
procedure WMActivate(var Message: TWMActivate); message WM_ACTIVATE;
procedure CMTextChanged(var Message: TMessage);
procedure WMWindowPosChanging(var Message: TWMWindowPosChanging); message WM_WINDOWPOSCHANGING;
procedure WMNCRButtonUp(var Message: TWMNCRButtonUp); message WM_NCRBUTTONUP;
private
{ Private declarations }
FWndFrameSize: Integer;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
uses
DWMAPI, CommCtrl, Themes, UXTheme, StdCtrls;
{$R *.dfm}
{$IF not Declared(UnicodeString)}
type
UnicodeString = WideString;
{$IFEND}
procedure DrawGlassCaption(Form: TForm; const Text: UnicodeString;
Color: TColor; var R: TRect; HorzAlignment: TAlignment = taLeftJustify;
VertAlignment: TTextLayout = tlCenter; ShowAccel: Boolean = False); overload;
const
BasicFormat = DT_SINGLELINE or DT_END_ELLIPSIS;
HorzFormat: array[TAlignment] of UINT = (DT_LEFT, DT_RIGHT, DT_CENTER);
VertFormat: array[TTextLayout] of UINT = (DT_TOP, DT_VCENTER, DT_BOTTOM);
AccelFormat: array[Boolean] of UINT = (DT_NOPREFIX, 0);
var
DTTOpts: TDTTOpts;
Element: TThemedWindow;
IsVistaAndMaximized: Boolean;
NCM: TNonClientMetrics;
ThemeData: HTHEME;
procedure DoTextOut;
begin
with ThemeServices.GetElementDetails(Element) do
DrawThemeTextEx(ThemeData, Form.Canvas.Handle, Part, State, PWideChar(Text),
Length(Text), BasicFormat or AccelFormat[ShowAccel] or
HorzFormat[HorzAlignment] or VertFormat[VertAlignment], #R, DTTOpts);
end;
begin
if Color = clNone then Exit;
IsVistaAndMaximized := (Form.WindowState = wsMaximized) and
(Win32MajorVersion = 6) and (Win32MinorVersion = 0);
ThemeData := OpenThemeData(0, 'CompositedWindow::Window');
Assert(ThemeData <> 0, SysErrorMessage(GetLastError));
Try
NCM.cbSize := SizeOf(NCM);
if SystemParametersInfo(SPI_GETNONCLIENTMETRICS, 0, #NCM, 0) then
if Form.BorderStyle in [bsToolWindow, bsSizeToolWin] then
Form.Canvas.Font.Handle := CreateFontIndirect(NCM.lfSmCaptionFont)
else
Form.Canvas.Font.Handle := CreateFontIndirect(NCM.lfCaptionFont);
ZeroMemory(#DTTOpts, SizeOf(DTTOpts));
DTTOpts.dwSize := SizeOf(DTTOpts);
DTTOpts.dwFlags := DTT_COMPOSITED or DTT_TEXTCOLOR;
if Color <> clDefault then
DTTOpts.crText := ColorToRGB(Color)
else if IsVistaAndMaximized then
DTTOpts.dwFlags := DTTOpts.dwFlags and not DTT_TEXTCOLOR
else if Form.Active then
DTTOpts.crText := GetSysColor(COLOR_CAPTIONTEXT)
else
DTTOpts.crText := GetSysColor(COLOR_INACTIVECAPTIONTEXT);
if not IsVistaAndMaximized then
begin
DTTOpts.dwFlags := DTTOpts.dwFlags or DTT_GLOWSIZE;
DTTOpts.iGlowSize := 15;
end;
if Form.WindowState = wsMaximized then
if Form.Active then
Element := twMaxCaptionActive
else
Element := twMaxCaptionInactive
else if Form.BorderStyle in [bsToolWindow, bsSizeToolWin] then
if Form.Active then
Element := twSmallCaptionActive
else
Element := twSmallCaptionInactive
else
if Form.Active then
Element := twCaptionActive
else
Element := twCaptionInactive;
DoTextOut;
if IsVistaAndMaximized then DoTextOut;
Finally
CloseThemeData(ThemeData);
end;
end;
function GetDwmBorderIconsRect(Form: TForm): TRect;
begin
if DwmGetWindowAttribute(Form.Handle, DWMWA_CAPTION_BUTTON_BOUNDS, #Result, SizeOf(Result)) <> S_OK then SetRectEmpty(Result);
end;
procedure DrawGlassCaption(Form: TForm; var R: TRect;
HorzAlignment: TAlignment = taLeftJustify; VertAlignment: TTextLayout = tlCenter;
ShowAccel: Boolean = False); overload;
begin
DrawGlassCaption(Form, Form.Caption, clDefault, R,
HorzAlignment, VertAlignment, ShowAccel);
end;
procedure TForm1.FormCreate(Sender: TObject);
var
R: TRect;
begin
if DwmCompositionEnabled then
begin
SetRectEmpty(R);
AdjustWindowRectEx(R, GetWindowLong(Handle, GWL_STYLE), False,
GetWindowLong(Handle, GWL_EXSTYLE));
FWndFrameSize := R.Right;
GlassFrame.Top := -R.Top;
GlassFrame.Enabled := True;
SetWindowPos(Handle, 0, Left, Top, Width, Height, SWP_FRAMECHANGED);
DoubleBuffered := True;
end;
end;
procedure TForm1.InvalidateTitleBar;
var
R: TRect;
begin
if not HandleAllocated then Exit;
R.Left := 0;
R.Top := 0;
R.Right := Width;
R.Bottom := GlassFrame.Top;
InvalidateRect(Handle, #R, False);
end;
procedure TForm1.CMTextChanged(var Message: TMessage);
begin
inherited;
InvalidateTitleBar;
end;
procedure TForm1.WMActivate(var Message: TWMActivate);
begin
inherited;
InvalidateTitleBar;
end;
procedure TForm1.WMNCHitTest(var Message: TWMNCHitTest);
var
ClientPos: TPoint;
IconRect: TRect;
begin
inherited;
if not GlassFrame.Enabled then Exit;
case Message.Result of
HTCLIENT:
HTMINBUTTON, HTMAXBUTTON, HTCLOSE:
begin
Message.Result := HTCAPTION;
Exit;
end;
else
Exit;
end;
ClientPos := ScreenToClient(Point(Message.XPos, Message.YPos));
if ClientPos.Y > GlassFrame.Top then Exit;
if ControlAtPos(ClientPos, True) <> nil then Exit;
IconRect := GetSysIconRect;
if (ClientPos.X < IconRect.Right) and ((WindowState = wsMaximized) or
((ClientPos.Y >= IconRect.Top) and (ClientPos.Y < IconRect.Bottom))) then
Message.Result := HTSYSMENU
else if ClientPos.Y < FWndFrameSize then
Message.Result := HTTOP
else
Message.Result := HTCAPTION;
end;
procedure ShowSystemMenu(Form: TForm; const Message: TWMNCRButtonUp);
var
Cmd: WPARAM;
Menu: HMENU;
procedure UpdateItem(ID: UINT; Enable: Boolean; MakeDefaultIfEnabled: Boolean = False);
const
Flags: array[Boolean] of UINT = (MF_GRAYED, MF_ENABLED);
begin
EnableMenuItem(Menu, ID, MF_BYCOMMAND or Flags[Enable]);
if MakeDefaultIfEnabled and Enable then
SetMenuDefaultItem(Menu, ID, MF_BYCOMMAND);
end;
begin
Menu := GetSystemMenu(Form.Handle, False);
if Form.BorderStyle in [bsSingle, bsSizeable, bsToolWindow, bsSizeToolWin] then
begin
SetMenuDefaultItem(Menu, UINT(-1), 0);
UpdateItem(SC_RESTORE, Form.WindowState <> wsNormal, True);
UpdateItem(SC_MOVE, Form.WindowState <> wsMaximized);
UpdateItem(SC_SIZE, (Form.WindowState <> wsMaximized) and
(Form.BorderStyle in [bsSizeable, bsSizeToolWin]));
UpdateItem(SC_MINIMIZE, (biMinimize in Form.BorderIcons) and
(Form.BorderStyle in [bsSingle, bsSizeable]));
UpdateItem(SC_MAXIMIZE, (biMaximize in Form.BorderIcons) and
(Form.BorderStyle in [bsSingle, bsSizeable]) and
(Form.WindowState <> wsMaximized), True);
end;
if Message.HitTest = HTSYSMENU then
SetMenuDefaultItem(Menu, SC_CLOSE, MF_BYCOMMAND);
Cmd := WPARAM(TrackPopupMenu(Menu, TPM_RETURNCMD or
GetSystemMetrics(SM_MENUDROPALIGNMENT), Message.XCursor,
Message.YCursor, 0, Form.Handle, nil));
PostMessage(Form.Handle, WM_SYSCOMMAND, Cmd, 0)
end;
procedure TForm1.WMWindowPosChanging(var Message: TWMWindowPosChanging);
const
SWP_STATECHANGED = $8000;
begin
if GlassFrame.Enabled then
if (Message.WindowPos.flags and SWP_STATECHANGED) = SWP_STATECHANGED then
Invalidate
else
InvalidateTitleBar;
inherited;
end;
procedure TForm1.WMNCRButtonUp(var Message: TWMNCRButtonUp);
begin
if not GlassFrame.Enabled or not (biSystemMenu in BorderIcons) then
inherited
else
case Message.HitTest of
HTCAPTION, HTSYSMENU: ShowSystemMenu(Self, Message);
else
inherited;
end;
end;
procedure TForm1.WndProc(var Message: TMessage);
begin
if GlassFrame.Enabled and HandleAllocated and DwmDefWindowProc(Handle,
Message.Msg, Message.WParam, Message.LParam, Message.Result) then
Exit;
inherited;
end;
procedure TForm1.PaintWindow(DC: HDC);
begin
with GetClientRect do
ExcludeClipRect(DC, 0, GlassFrame.Top, Right, Bottom);
inherited;
end;
procedure TForm1.SpeedButton1Click(Sender: TObject);
begin
Close;
end;
procedure TForm1.FormPaint(Sender: TObject);
var
IconHandle: HICON;
R: TRect;
begin
if ImageList1.Count = 0 then
begin
ImageList1.Width := GetSystemMetrics(SM_CXSMICON);
ImageList1.Height := GetSystemMetrics(SM_CYSMICON);
{$IF NOT DECLARED(TColorDepth)}
ImageList1.Handle := ImageList_Create(ImageList1.Width,
ImageList1.Height, ILC_COLOR32 or ILC_MASK, 1, 1);
{$IFEND}
IconHandle := Icon.Handle;
if IconHandle = 0 then IconHandle := Application.Icon.Handle;
ImageList_AddIcon(ImageList1.Handle, IconHandle);
end;
R := GetSysIconRect;
ImageList1.Draw(Canvas, R.Left, R.Top, 0);
R.Left := R.Right + FWndFrameSize - 3;
if WindowState = wsMaximized then
R.Top := FWndFrameSize
else
R.Top := 0;
R.Right := GetDwmBorderIconsRect(Self).Left - FWndFrameSize - 1;
R.Bottom := GlassFrame.Top;
DrawGlassCaption(Self, R);
end;
function TForm1.GetSysIconRect: TRect;
begin
if not (biSystemMenu in BorderIcons) or not (BorderStyle in [bsSingle, bsSizeable]) then
SetRectEmpty(Result)
else
begin
Result.Left := 0;
Result.Right := GetSystemMetrics(SM_CXSMICON);
Result.Bottom := GetSystemMetrics(SM_CYSMICON);
if WindowState = wsMaximized then
Result.Top := GlassFrame.Top - Result.Bottom - 2
else
Result.Top := 6;
Inc(Result.Bottom, Result.Top);
end;
end;
procedure TForm1.WMNCCalcSize(var Message: TWMNCCalcSize);
begin
if not GlassFrame.Enabled then
inherited
else
with Message.CalcSize_Params.rgrc[0] do
begin
Inc(Left, FWndFrameSize);
Dec(Right, FWndFrameSize);
Dec(Bottom, FWndFrameSize);
end;
end;
end.
Please help me find what is causing the Caption Buttons to become unresponsive to mouse clicks.
The standard buttons do not work because your WM_NCHITTEST handler is returning HTCAPTION for them. You are lying to Windows, telling it that the mouse is not over the buttons even if it really is. If the inherited handler returns HTMINBUTTON, HTMAXBUTTON, or HTCLOSE, just Exit without modifying the Message.Result:
procedure TForm1.WMNCHitTest(var Message: TWMNCHitTest);
var
ClientPos: TPoint;
IconRect: TRect;
begin
inherited;
if not GlassFrame.Enabled then Exit;
case Message.Result of
HTCLIENT:
HTMINBUTTON, HTMAXBUTTON, HTCLOSE:
begin
//Message.Result := HTCAPTION; // <-- here
Exit;
end;
else
Exit;
end;
...
end;

SendKeys from WM_HOTKEY handler

Have AppActivate and SendKeys functions.
When use: AppActivate('*WordPad'); SendKeys('Test");
this works fine - application activated and text pasted
but then use it from WM_HOTKEY handler from the same program,
this is not worked.
Any ideas?
procedure wm_hotkeyhandler(var Msg: TWMHotkey); message WM_HOTKEY;
...
procedure TFormMain.wm_hotkeyhandler(var Msg: TWMHotkey);
var
Pt: TPoint;
begin
inherited;
if (Msg.HotKey = HotKeyId_L) then SendKeys('Test'); // not pasted to active app
if (Msg.HotKey = HotKeyId_M) then begin
// invoke context menu and paste text after click to menu item, works fine
GetCursorPos(Pt);
popPaste.Popup(Pt.x, Pt.y);
end;
end;
Update 1:
// this code works fine
procedure TFormTest.btnAppActivateClick(Sender: TObject);
var
sTitle, sKeys: string;
begin
sTitle := '*WordPad';
sKeys := 'Hello{ENTER}World!';
AppActivate(sTitle);
SendKeys(PChar(sKeys), False);
end;
menu item handler (which invoked by hotkey HotKeyId_M):
procedure TFormMain.mnPasteLoginClick(Sender: TObject);
begin
SendKeys('Hello{ENTER}World!', False);
end;
hotkeys:
HotKeyId_L: Integer;
HotKeyId_M: Integer;
initialization of hotkeys:
HotKeyId_L := GlobalAddAtom('HotKeyL');
RegisterHotKey(Handle, HotKeyId_L, MOD_CONTROL + MOD_ALT, Byte('L'));
HotKeyId_M := GlobalAddAtom('HotKeyM');
RegisterHotKey(Handle, HotKeyId_L, MOD_CONTROL + MOD_ALT, Byte('M'));
Update 2: (full code for test)
unit Unit2;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, Menus;
type
TForm2 = class(TForm)
btnActivate: TButton;
popPopup: TPopupMenu;
Paste1: TMenuItem;
procedure btnActivateClick(Sender: TObject);
procedure Paste1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
HotKeyId_L: Integer;
HotKeyId_M: Integer;
procedure wm_hotkeyhandler(var Msg: TWMHotkey); message WM_HOTKEY;
public
{ Public declarations }
end;
var
Form2: TForm2;
implementation
{$R *.DFM}
type
TCompareDirection = (cdHead, cdTail, cdNone);
TWindowObj = class(TObject)
private
targetTitle : PChar;
compareLength: Integer;
FCompareDirection: TCompareDirection;
FWindowHandle: THandle;
public
constructor Create;
destructor Destroy; override;
function Equal(ATitle: PChar): Boolean;
function SetTitle(const Title: string ): Boolean;
property WindowHandle: THandle read FWindowHandle write FWindowHandle;
end;
function EnumWindowsProc(hWnd: HWND; lParam: LPARAM):Bool; export; stdcall;
var
WinObj: TWindowObj;
aWndName: array[0..MAX_PATH] of Char;
begin
Result := True;
WinObj := TWindowObj(lParam);
GetWindowText(hWnd, aWndName, MAX_PATH);
if WinObj.Equal(aWndName) then begin
WinObj.WindowHandle := hWnd;
Result := False; // Stop Enumerate
end;
end;
function GetWindowHandleByTitle(const Title: string): THandle;
var
WinObj: TWindowObj;
begin
Result := 0;
WinObj := TWindowObj.Create;
try
if WinObj.SetTitle(Title) then begin
EnumWindows(#EnumWindowsProc, Integer(WinObj));
Result := WinObj.WindowHandle;
end;
finally
WinObj.Free;
end;
end;
function AppActivate(const Title: string ): Boolean;
var
hWnd: THandle;
begin
hWnd := GetWindowHandleByTitle(Title);
Result := (hWnd > 0);
if Result then begin
SendMessage(hWnd, WM_SYSCOMMAND, SC_HOTKEY, hWnd);
SendMessage(hWnd, WM_SYSCOMMAND, SC_RESTORE, hWnd);
SetForegroundWindow(hWnd);
end;
end;
constructor TWindowObj.Create;
begin
TargetTitle := nil;
FWindowHandle := 0;
end;
destructor TWindowObj.Destroy;
begin
inherited Destroy;
if Assigned(TargetTitle) then
StrDispose(TargetTitle) ;
end;
function TWindowObj.Equal(ATitle: PChar): Boolean;
var
p : Pchar;
stringLength : integer;
begin
Result := False;
if (TargetTitle = nil) then
Exit;
case FCompareDirection of
cdHead: begin
if StrLIComp(ATitle, TargetTitle, compareLength) = 0 then
Result := True;
end;
cdTail: begin
stringLength := StrLen(ATitle);
p := #ATitle[stringLength - compareLength];
if (StrLIComp(p, Targettitle, compareLength) = 0) then
Result := True;
end;
cdNone: begin
Result := True;
end;
end;
end;
function TWindowObj.SetTitle(const Title: string ): Boolean;
var
pTitle, p: PChar;
begin
Result := False;
pTitle := StrAlloc(Length(Title) + 1);
StrPCopy(pTitle, Title);
p := StrScan(pTitle, '*');
if Assigned(p) then begin
if StrLen(pTitle) = 1 then begin {full matching }
FCompareDirection := cdNone;
compareLength := 0;
TargetTitle := nil;
StrDispose(pTitle);
end
else
if (p = pTitle) then begin {tail matching }
Inc(p);
if StrScan(p, '*') <> nil then begin
{MessageDlg( 'Please 1 wild char ', mtError, [mbOK],0 ); }
StrDispose( pTitle);
TargetTitle := nil;
FCompareDirection := cdNone;
Comparelength := 0;
exit;
end;
FCompareDirection := cdTail;
CompareLength := StrLen(PTitle) - 1;
TargetTitle := StrAlloc(StrLen(p) + 1 );
StrCopy(targetTitle, p);
StrDispose(PTitle);
end
else begin
p^ := #0;
FCompareDirection := cdHead;
CompareLength := Strlen( pTitle );
Targettitle := pTitle;
end;
end
else begin
FCompareDirection := cdHead;
compareLength := Strlen( pTitle );
TargetTitle := pTitle;
end;
Result := True;
end;
//========================================
// SendKeys
//
// Converts a string of characters and key names to keyboard events and passes them to Windows.
//
// Example syntax:
// SendKeys('abc123{left}{left}{left}def{end}456{left 6}ghi{end}789', True);
function SendKeys(SendStr: PChar; Wait: Boolean): Boolean;
type
WBytes = array[0..pred(SizeOf(Word))] of Byte;
TSendKey = record
Name : ShortString;
VKey : Byte;
end;
const
// Array of keys that SendKeys recognizes.
// If you add to this list, you must be sure to keep it sorted alphabetically
// by Name because a binary search routine is used to scan it.}
MaxSendKeyRecs = 41;
SendKeyRecs : array[1..MaxSendKeyRecs] of TSendKey = (
(Name:'BACKSPACE'; VKey:VK_BACK),
(Name:'BKSP'; VKey:VK_BACK),
(Name:'BREAK'; VKey:VK_CANCEL),
(Name:'BS'; VKey:VK_BACK),
(Name:'CAPSLOCK'; VKey:VK_CAPITAL),
(Name:'CLEAR'; VKey:VK_CLEAR),
(Name:'DEL'; VKey:VK_DELETE),
(Name:'DELETE'; VKey:VK_DELETE),
(Name:'DOWN'; VKey:VK_DOWN),
(Name:'END'; VKey:VK_END),
(Name:'ENTER'; VKey:VK_RETURN),
(Name:'ESC'; VKey:VK_ESCAPE),
(Name:'ESCAPE'; VKey:VK_ESCAPE),
(Name:'F1'; VKey:VK_F1),
(Name:'F10'; VKey:VK_F10),
(Name:'F11'; VKey:VK_F11),
(Name:'F12'; VKey:VK_F12),
(Name:'F13'; VKey:VK_F13),
(Name:'F14'; VKey:VK_F14),
(Name:'F15'; VKey:VK_F15),
(Name:'F16'; VKey:VK_F16),
(Name:'F2'; VKey:VK_F2),
(Name:'F3'; VKey:VK_F3),
(Name:'F4'; VKey:VK_F4),
(Name:'F5'; VKey:VK_F5),
(Name:'F6'; VKey:VK_F6),
(Name:'F7'; VKey:VK_F7),
(Name:'F8'; VKey:VK_F8),
(Name:'F9'; VKey:VK_F9),
(Name:'HELP'; VKey:VK_HELP),
(Name:'HOME'; VKey:VK_HOME),
(Name:'INS'; VKey:VK_INSERT),
(Name:'LEFT'; VKey:VK_LEFT),
(Name:'NUMLOCK'; VKey:VK_NUMLOCK),
(Name:'PGDN'; VKey:VK_NEXT),
(Name:'PGUP'; VKey:VK_PRIOR),
(Name:'PRTSC'; VKey:VK_PRINT),
(Name:'RIGHT'; VKey:VK_RIGHT),
(Name:'SCROLLLOCK'; VKey:VK_SCROLL),
(Name:'TAB'; VKey:VK_TAB),
(Name:'UP'; VKey:VK_UP)
);
{Extra VK constants missing from Delphi's Windows API interface}
VK_NULL=0;
VK_SemiColon=186;
VK_Equal=187;
VK_Comma=188;
VK_Minus=189;
VK_Period=190;
VK_Slash=191;
VK_BackQuote=192;
VK_LeftBracket=219;
VK_BackSlash=220;
VK_RightBracket=221;
VK_Quote=222;
VK_Last=VK_Quote;
ExtendedVKeys : set of byte =
[VK_Up,
VK_Down,
VK_Left,
VK_Right,
VK_Home,
VK_End,
VK_Prior, {PgUp}
VK_Next, {PgDn}
VK_Insert,
VK_Delete];
const
INVALIDKEY = $FFFF {Unsigned -1};
VKKEYSCANSHIFTON = $01;
VKKEYSCANCTRLON = $02;
VKKEYSCANALTON = $04;
UNITNAME = 'SendKeys';
var
UsingParens, ShiftDown, ControlDown, AltDown, FoundClose : Boolean;
PosSpace : Byte;
I, L : Integer;
NumTimes, MKey : Word;
KeyString : String[20];
procedure DisplayMessage(Msg: PChar);
begin
MessageBox(0, Msg, UNITNAME, 0);
end;
function BitSet(BitTable, BitMask: Byte): Boolean;
begin
Result := ByteBool(BitTable and BitMask);
end;
procedure SetBit(var BitTable : Byte; BitMask : Byte);
begin
BitTable:=BitTable or Bitmask;
end;
procedure KeyboardEvent(VKey, ScanCode: Byte; Flags: DWORD);
var
KeyboardMsg : TMsg;
begin
keybd_event(VKey, ScanCode, Flags, 0);
if Wait then
while PeekMessage(KeyboardMsg, 0, WM_KEYFIRST, WM_KEYLAST, PM_REMOVE) do begin
TranslateMessage(KeyboardMsg);
DispatchMessage(KeyboardMsg);
end;
end;
procedure SendKeyDown(VKey: Byte; NumTimes: Word; GenUpMsg: Boolean);
var
Cnt: Word;
ScanCode: Byte;
NumState: Boolean;
KeyBoardState: TKeyboardState;
begin
if (VKey = VK_NUMLOCK) then begin
NumState := ByteBool(GetKeyState(VK_NUMLOCK) and 1);
GetKeyBoardState(KeyBoardState);
if NumState then
KeyBoardState[VK_NUMLOCK] := (KeyBoardState[VK_NUMLOCK] and not 1)
else
KeyBoardState[VK_NUMLOCK] := (KeyBoardState[VK_NUMLOCK] or 1);
SetKeyBoardState(KeyBoardState);
Exit;
end;
ScanCode := Lo(MapVirtualKey(VKey, 0));
for Cnt := 1 to NumTimes do
if (VKey in ExtendedVKeys) then begin
KeyboardEvent(VKey, ScanCode, KEYEVENTF_EXTENDEDKEY);
if GenUpMsg then
KeyboardEvent(VKey, ScanCode, KEYEVENTF_EXTENDEDKEY or KEYEVENTF_KEYUP)
end
else begin
KeyboardEvent(VKey, ScanCode, 0);
if GenUpMsg then
KeyboardEvent(VKey, ScanCode, KEYEVENTF_KEYUP);
end;
end;
procedure SendKeyUp(VKey: Byte);
var
ScanCode : Byte;
begin
ScanCode := Lo(MapVirtualKey(VKey, 0));
if (VKey in ExtendedVKeys)then
KeyboardEvent(VKey, ScanCode, KEYEVENTF_EXTENDEDKEY and KEYEVENTF_KEYUP)
else
KeyboardEvent(VKey, ScanCode, KEYEVENTF_KEYUP);
end;
procedure SendKey(MKey: Word; NumTimes: Word; GenDownMsg: Boolean);
begin
if BitSet(Hi(MKey), VKKEYSCANSHIFTON) then SendKeyDown(VK_SHIFT, 1, False);
if BitSet(Hi(MKey), VKKEYSCANCTRLON) then SendKeyDown(VK_CONTROL, 1, False);
if BitSet(Hi(MKey), VKKEYSCANALTON) then SendKeyDown(VK_MENU, 1, False);
SendKeyDown(Lo(MKey), NumTimes, GenDownMsg);
if BitSet(Hi(MKey), VKKEYSCANSHIFTON) then SendKeyUp(VK_SHIFT);
if BitSet(Hi(MKey), VKKEYSCANCTRLON) then SendKeyUp(VK_CONTROL);
if BitSet(Hi(MKey), VKKEYSCANALTON) then SendKeyUp(VK_MENU);
end;
// Implements a simple binary search to locate special key name strings
function StringToVKey(KeyString: ShortString): Word;
var
Found, Collided : Boolean;
Bottom, Top, Middle : Byte;
begin
Result := INVALIDKEY;
Bottom := 1;
Top := MaxSendKeyRecs;
Found := False;
Middle := (Bottom + Top) div 2;
repeat
Collided:=((Bottom=Middle) or (Top=Middle));
if (KeyString=SendKeyRecs[Middle].Name) then begin
Found:=True;
Result:=SendKeyRecs[Middle].VKey;
end
else begin
if (KeyString>SendKeyRecs[Middle].Name) then
Bottom:=Middle
else
Top:=Middle;
Middle:=(Succ(Bottom+Top)) div 2;
end;
until (Found or Collided);
if (Result = INVALIDKEY) then
DisplayMessage('Invalid Key Name');
end;
procedure PopUpShiftKeys;
begin
if (not UsingParens) then begin
if ShiftDown then SendKeyUp(VK_SHIFT);
if ControlDown then SendKeyUp(VK_CONTROL);
if AltDown then SendKeyUp(VK_MENU);
ShiftDown := False;
ControlDown := False;
AltDown := False;
end;
end;
var
AllocationSize : integer;
begin
AllocationSize := MaxInt;
Result := False;
UsingParens := False;
ShiftDown := False;
ControlDown := False;
AltDown := False;
I := 0;
L := StrLen(SendStr);
if (L > AllocationSize) then
L := AllocationSize;
if (L = 0) then
Exit;
while (I < L) do begin
case SendStr[I] of
'(': begin
UsingParens := True;
Inc(I);
end;
')': begin
UsingParens := False;
PopUpShiftKeys;
Inc(I);
end;
'%': begin
AltDown := True;
SendKeyDown(VK_MENU, 1, False);
Inc(I);
end;
'+': begin
ShiftDown := True;
SendKeyDown(VK_SHIFT, 1, False);
Inc(I);
end;
'^': begin
ControlDown := True;
SendKeyDown(VK_CONTROL, 1, False);
Inc(I);
end;
'{': begin
NumTimes := 1;
if (SendStr[Succ(I)] = '{') then begin
MKey := VK_LEFTBRACKET;
SetBit(WBytes(MKey)[1], VKKEYSCANSHIFTON);
SendKey(MKey, 1, True);
PopUpShiftKeys;
Inc(I, 3);
Continue;
end;
KeyString := '';
FoundClose := False;
while (I <= L) do begin
Inc(I);
if (SendStr[I] = '}') then begin
FoundClose := True;
Inc(I);
Break;
end;
KeyString := KeyString + Upcase(SendStr[I]);
end;
if Not FoundClose then begin
DisplayMessage('No Close');
Exit;
end;
if (SendStr[I] = '}') then begin
MKey := VK_RIGHTBRACKET;
SetBit(Wbytes(MKey)[1], VKKEYSCANSHIFTON);
SendKey(MKey, 1, True);
PopUpShiftKeys;
Inc(I);
Continue;
end;
PosSpace:=Pos(' ', KeyString);
if (PosSpace <> 0) then begin
NumTimes := StrToInt(Copy(KeyString, Succ(PosSpace), Length(KeyString) - PosSpace));
KeyString := Copy(KeyString, 1, Pred(PosSpace));
end;
If (Length(KeyString)=1) then
MKey := vkKeyScan(KeyString[1])
else
MKey := StringToVKey(KeyString);
If (MKey <> INVALIDKEY) then begin
SendKey(MKey, NumTimes, True);
PopUpShiftKeys;
Continue;
end;
end;
'~': begin
SendKeyDown(VK_RETURN, 1, True);
PopUpShiftKeys;
Inc(I);
end;
else
MKey := vkKeyScan(SendStr[I]);
if (MKey <> INVALIDKEY) then begin
SendKey(MKey, 1, True);
PopUpShiftKeys;
end
else
DisplayMessage('Invalid KeyName');
Inc(I);
end;
end;
Result := True;
PopUpShiftKeys;
end;
procedure TForm2.btnActivateClick(Sender: TObject);
var
sTitle, sKeys: string;
begin
sTitle := '*WordPad';
sKeys := 'Hello{ENTER}World!';
AppActivate(sTitle);
SendKeys(PChar(sKeys), False);
end;
procedure TForm2.Paste1Click(Sender: TObject);
begin
SendKeys('Hello{ENTER}World!', False);
end;
procedure TForm2.FormCreate(Sender: TObject);
begin
HotKeyId_L := GlobalAddAtom('HotKeyP');
RegisterHotKey(Handle, HotKeyId_L, MOD_CONTROL or MOD_ALT, Byte('L'));
HotKeyId_M := GlobalAddAtom('HotKeyM');
RegisterHotKey(Handle, HotKeyId_M, MOD_CONTROL or MOD_ALT, Byte('M'));
end;
procedure TForm2.FormDestroy(Sender: TObject);
begin
UnregisterHotKey(Handle, HotKeyId_L);
GlobalDeleteAtom(HotKeyId_L);
end;
procedure TForm2.wm_hotkeyhandler(var Msg: TWMHotkey);
var
Pt: TPoint;
begin
inherited;
if (Msg.HotKey = HotKeyId_L) then SendKeys('Hello{ENTER}World!', False);
if (Msg.HotKey = HotKeyId_M) then begin
GetCursorPos(Pt);
popPopup.Popup(Pt.x, Pt.y);
end;
end;
end.

Download file from internet within a thread in Delphi

How to download a file from internet with progress bar using threads in Delphi 2009/10 without Indy components?
I don't like to use indy either, my reason is it is too large. You could also use wininet. I have written the following for a small project required small app size.
unit wininetUtils;
interface
uses Windows, WinInet
{$IFDEF KOL}
,KOL
{$ELSE}
,Classes
{$ENDIF}
;
type
{$IFDEF KOL}
_STREAM = PStream;
_STRLIST = PStrList;
{$ELSE}
_STREAM = TStream;
_STRLIST = TStrings;
{$ENDIF}
TProgressCallback = function (ATotalSize, ATotalRead, AStartTime: DWORD): Boolean;
function DownloadToFile(const AURL: String; const AFilename: String;
const AAgent: String = '';
const AHeaders: _STRLIST = nil;
const ACallback: TProgressCallback = nil
) : LongInt;
function DownloadToStream(AURL: String; AStream: _STREAM;
const AAgent: String = '';
const AHeaders: _STRLIST = nil;
const ACallback: TProgressCallback = nil
) : LongInt;
implementation
function DownloadToFile(const AURL: String; const AFilename: String;
const AAgent: String = '';
const AHeaders: _STRLIST = nil;
const ACallback: TProgressCallback = nil
) : LongInt;
var
FStream: _STREAM;
begin
{$IFDEF KOL}
// fStream := NewFileStream(AFilename, ofCreateNew or ofOpenWrite);
// fStream := NewWriteFileStream(AFilename);
fStream := NewMemoryStream;
{$ELSE}
fStream := TFileStream.Create(AFilename, fmCreate);
// _STRLIST = TStrings;
{$ENDIF}
try
Result := DownloadToStream(AURL, FStream, AAgent, AHeaders, ACallback);
fStream.SaveToFile(AFilename, 0, fStream.Size);
finally
fStream.Free;
end;
end;
function StrToIntDef(const S: string; Default: Integer): Integer;
var
E: Integer;
begin
Val(S, Result, E);
if E <> 0 then Result := Default;
end;
function DownloadToStream(AURL: String; AStream: _STREAM;
const AAgent: String = '';
const AHeaders: _STRLIST = nil;
const ACallback: TProgressCallback = nil
) : LongInt;
function _HttpQueryInfo(AFile: HINTERNET; AInfo: DWORD): string;
var
infoBuffer: PChar;
dummy: DWORD;
err, bufLen: DWORD;
res: LongBool;
begin
Result := '';
bufLen := 0;
dummy := 0;
infoBuffer := nil;
res := HttpQueryInfo(AFile, AInfo, infoBuffer, bufLen, dummy);
if not res then
begin
// Probably working offline, or no internet connection.
err := GetLastError;
if err = ERROR_HTTP_HEADER_NOT_FOUND then
begin
// No headers
end else if err = ERROR_INSUFFICIENT_BUFFER then
begin
GetMem(infoBuffer, bufLen);
try
HttpQueryInfo(AFile, AInfo, infoBuffer, bufLen, dummy);
Result := infoBuffer;
finally
FreeMem(infoBuffer);
end;
end;
end;
end;
procedure ParseHeaders;
begin
end;
const
BUFFER_SIZE = 16184;
var
buffer: array[1..BUFFER_SIZE] of byte;
Totalbytes, Totalread, bytesRead, StartTime: DWORD;
hInet: HINTERNET;
reply: String;
hFile: HINTERNET;
begin
Totalread := 0;
Result := 0;
hInet := InternetOpen(PChar(AAgent), INTERNET_OPEN_TYPE_PRECONFIG, nil,nil,0);
if hInet = nil then Exit;
try
hFile := InternetOpenURL(hInet, PChar(AURL), nil, 0, 0, 0);
if hFile = nil then Exit;
StartTime := GetTickCount;
try
if AHeaders <> nil then
begin
AHeaders.Text := _HttpQueryInfo(hFile, HTTP_QUERY_RAW_HEADERS_CRLF);
ParseHeaders;
end;
Totalbytes := StrToIntDef(_HttpQueryInfo(hFile,
HTTP_QUERY_CONTENT_LENGTH), 0);
reply := _HttpQueryInfo(hFile, HTTP_QUERY_STATUS_CODE);
if reply = '200' then
// File exists, all ok.
result := 200
else if reply = '401' then
// Not authorised. Assume page exists,
// but we can't check it.
result := 401
else if reply = '404' then
// No such file.
result := 404
else if reply = '500' then
// Internal server error.
result := 500
else
Result := StrToIntDef(reply, 0);
repeat
InternetReadFile(hFile, #buffer, SizeOf(buffer), bytesRead);
if bytesRead > 0 then
begin
AStream.Write(buffer, bytesRead);
Inc(Totalread, bytesRead);
if Assigned(ACallback) then
begin
if not ACallback(TotalBytes, Totalread, StartTime) then Break;
end;
Sleep(10);
end;
// BlockWrite(localFile, buffer, bytesRead);
until bytesRead = 0;
finally
InternetCloseHandle(hFile);
end;
finally
InternetCloseHandle(hInet);
end;
end;
end.
This uses the clever internet suite to handle the download, I haven't so much as checked it in the IDE so I wouldn't expect it to compile and no doubt it's full of errors but it should be enough to get you started.
I don't know why you don't want to use Indy but I would strongly advise getting some components to help with the Http download... there really is no need to reinvent the wheel.
interface
type
TMyDownloadThread= Class(TThread)
private
FUrl: String;
FFileName: String;
FProgressHandle: HWND;
procedure GetFile (Url: String; Stream: TStream; ReceiveProgress: TclSocketProgressEvent);
procedure OnReceiveProgress(Sender: TObject; ABytesProceed, ATotalBytes: Integer);
procedure SetPercent(Percent: Double);
protected
Procedure Execute; Override;
public
Constructor Create(Url, FileName: String; PrograssHandle: HWND);
End;
implementation
constructor TMyDownloadThread.Create(Url, FileName: String; PrograssHandle: HWND);
begin
Inherited Create(True);
FUrl:= Url;
FFileName:= FileName;
FProgressHandle:= PrograssHandle;
Resume;
end;
procedure TMyDownloadThread.GetFile(Url: String; Stream: TStream; ReceiveProgress: TclSocketProgressEvent);
var
Http: TclHttp;
begin
Http := TclHTTP.Create(nil);
try
try
Http.OnReceiveProgress := ReceiveProgress;
Http.Get(Url, Stream);
except
end;
finally
Http.Free;
end;
end;
procedure TMyDownloadThread.OnReceiveProgress(Sender: TObject; ABytesProceed, ATotalBytes: Integer);
begin
SetPercent((ABytesProceed / ATotalBytes) * 100);
end;
procedure TMyDownloadThread.SetPercent(Percent: Double);
begin
PostMessage(FProgressHandle, AM_DownloadPercent, LowBytes(Percent), HighBytes(Percent));
end;
procedure TMyDownloadThread.Execute;
var
FileStream: TFileStream;
begin
FileStream := TFileStream.Create(FFileName, fmCreate);
try
GetFile(FUrl, FileStream, OnReceiveProgress);
finally
FileStream.Free;
end;
end;

How to make MessageDlg centered on owner form

I'd like that MessageDlg appear centered on its parent form.
Any suggestions on how to accomplish this in Delphi 2010?
I found the code below here: http://delphi.about.com/od/formsdialogs/l/aa010304a.htm but it's not working for me. The pop-up still is not centered on the owner form. (It's not clear to me how the method would actually know the owner form...)
function TForm1.MessageDlg(const Msg: string; DlgType: TMsgDlgType;
Buttons: TMsgDlgButtons; HelpCtx: Integer): Integer;
begin
with CreateMessageDialog(Msg, DlgType, Buttons) do
try
Position := poOwnerFormCenter;
Result := ShowModal
finally
Free
end
end;
The dialog doesn't have a relationship with the instance of TForm1. It would not be hard to set the position of the form manually, but I bet someone who is more familiar with this area of the VCL will know how to do it a cleaner way.
Personally I never use the Position property and use my own code to position all my forms because I've never been satisfied with the performance of the Position property.
UPDATE: You can change the owner of the dialog using Self.InsertComponent(Dialog). You'd have to store your dialog into a local variable, say, Dialog, for this to work:
function TForm1.MessageDlg(const Msg: string; DlgType: TMsgDlgType;
Buttons: TMsgDlgButtons; HelpCtx: Integer): Integer;
var
Dialog: TForm;
begin
Dialog := CreateMessageDialog(Msg, DlgType, Buttons);
try
Self.InsertComponent(Dialog);
Dialog.Position := poOwnerFormCenter;
Result := Dialog.ShowModal
finally
Dialog.Free
end
end;
You can do
function MessageDlg(const AOwner: TForm; const Msg: string; DlgType: TMsgDlgType;
Buttons: TMsgDlgButtons; HelpCtx: Integer = 0): Integer;
begin
with CreateMessageDialog(Msg, DlgType, Buttons) do
try
Left := AOwner.Left + (AOwner.Width - Width) div 2;
Top := AOwner.Top + (AOwner.Height - Height) div 2;
Result := ShowModal;
finally
Free;
end
end;
and call it like
procedure TForm1.FormClick(Sender: TObject);
begin
MessageDlg(Self, 'This is a test', mtInformation, [mbOK]);
end;
However, I would personally not do this, because the dialog shown by CreateMessageDialog is not a native Windows dialog. Compare the visual result with the native stuff:
procedure TForm1.FormClick(Sender: TObject);
begin
case MessageBox(Handle, PChar('This is a test. Do you wish to do something?'), PChar('A Silly Example'), MB_ICONQUESTION or MB_YESNO) of
ID_YES:
MessageBox(Handle, PChar('Great!'), PChar('A Silly Example'), MB_ICONINFORMATION or MB_OK);
ID_NO:
MessageBox(Handle, PChar('OK, well, I cannot force you...'), PChar('A Silly Example'), MB_ICONINFORMATION or MB_OK);
end;
end;
At least in Windows 7 with the Aero theme enabled, the native dialog looks much better. However, it seems, this cannot be centered over any particular form. Instead, the dialog is centered on the current monitor. But this is also the default behaviour in Windows (try Notepad, WordPad, or Paint), so why do you need this new behaviour?
Why limit this desire to message dialogs? Like David Heffernan commented:
Native dialogs always win!
With the following unit(s), you can center any native dialog, such as: MessageBox, TFindDialog, TOpenDialog, TFontDialog, TPrinterSetupDialog, etc... The main unit provides two routines, both with some optional parameters:
function ExecuteCentered(Dialog: TCommonDialog;
WindowToCenterIn: HWND = 0): Boolean;
function MsgBox(const Text: String; Flags: Cardinal = DefFlags;
const Caption: String = DefCaption;
WindowToCenterIn: HWND = 0): Integer;
Wherelse you would use OpenDialog1.Execute and let Windows decide where to show the dialog, you now use ExecuteCentered(OpenDialog1) and the dialog is centered in the screen's active form:
To show message dialogs, use MsgBox, a wrapper around Application.MessageBox (which in turn is a wrapper around Windows.MessageBox). Some examples:
MsgBox('Hello world!');
MsgBox('Cancel saving?', MB_YESNO or MB_ICONQUESTION or MB_DEFBUTTON2);
MsgBox('Please try again.', MB_OK, 'Error');
MsgBox('I''m centered in the toolbar.', MB_OK, 'Fun!', Toolbar1.Handle);
The units:
unit AwDialogs;
interface
uses
Dialogs, Forms, Windows, Controls, Messages, AwHookInstance, Math, MultiMon;
const
DefCaption = 'Application.Title';
DefFlags = MB_OK;
procedure CenterWindow(WindowToStay, WindowToCenter: HWND);
function GetTopWindow: HWND;
function ExecuteCentered(Dialog: TCommonDialog;
WindowToCenterIn: HWND = 0): Boolean;
function MsgBox(const Text: String; Flags: Cardinal = DefFlags;
const Caption: String = DefCaption;
WindowToCenterIn: HWND = 0): Integer;
implementation
procedure CenterWindow(WindowToStay, WindowToCenter: HWND);
var
R1: TRect;
R2: TRect;
Monitor: HMonitor;
MonInfo: TMonitorInfo;
MonRect: TRect;
X: Integer;
Y: Integer;
begin
GetWindowRect(WindowToStay, R1);
GetWindowRect(WindowToCenter, R2);
Monitor := MonitorFromWindow(WindowToStay, MONITOR_DEFAULTTONEAREST);
MonInfo.cbSize := SizeOf(MonInfo);
GetMonitorInfo(Monitor, #MonInfo);
MonRect := MonInfo.rcWork;
with R1 do
begin
X := (Right - Left - R2.Right + R2.Left) div 2 + Left;
Y := (Bottom - Top - R2.Bottom + R2.Top) div 2 + Top;
end;
X := Max(MonRect.Left, Min(X, MonRect.Right - R2.Right + R2.Left));
Y := Max(MonRect.Top, Min(Y, MonRect.Bottom - R2.Bottom + R2.Top));
SetWindowPos(WindowToCenter, 0, X, Y, 0, 0, SWP_NOACTIVATE or
SWP_NOOWNERZORDER or SWP_NOSIZE or SWP_NOZORDER);
end;
function GetTopWindow: HWND;
begin
Result := GetLastActivePopup(Application.Handle);
if (Result = Application.Handle) or not IsWindowVisible(Result) then
Result := Screen.ActiveCustomForm.Handle;
end;
{ TAwCommonDialog }
type
TAwCommonDialog = class(TObject)
private
FCenterWnd: HWND;
FDialog: TCommonDialog;
FHookProc: TFarProc;
FWndHook: HHOOK;
procedure HookProc(var Message: THookMessage);
function Execute: Boolean;
end;
function TAwCommonDialog.Execute: Boolean;
begin
try
Application.NormalizeAllTopMosts;
FHookProc := MakeHookInstance(HookProc);
FWndHook := SetWindowsHookEx(WH_CALLWNDPROCRET, FHookProc, 0,
GetCurrentThreadID);
Result := FDialog.Execute;
finally
if FWndHook <> 0 then
UnhookWindowsHookEx(FWndHook);
if FHookProc <> nil then
FreeHookInstance(FHookProc);
Application.RestoreTopMosts;
end;
end;
procedure TAwCommonDialog.HookProc(var Message: THookMessage);
var
Data: PCWPRetStruct;
Parent: HWND;
begin
with Message do
if nCode < 0 then
Result := CallNextHookEx(FWndHook, nCode, wParam, lParam)
else
Result := 0;
if Message.nCode = HC_ACTION then
begin
Data := PCWPRetStruct(Message.lParam);
if (FDialog.Handle <> 0) and (Data.message = WM_SHOWWINDOW) then
begin
Parent := GetWindowLong(FDialog.Handle, GWL_HWNDPARENT);
if ((Data.hwnd = FDialog.Handle) and (Parent = Application.Handle)) or
((Data.hwnd = FDialog.Handle) and (FDialog is TFindDialog)) or
(Data.hwnd = Parent) then
begin
CenterWindow(FCenterWnd, Data.hwnd);
SetWindowPos(Data.hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE or
SWP_NOSIZE or SWP_NOACTIVATE or SWP_NOOWNERZORDER);
UnhookWindowsHookEx(FWndHook);
FWndHook := 0;
FreeHookInstance(FHookProc);
FHookProc := nil;
end;
end;
end;
end;
function ExecuteCentered(Dialog: TCommonDialog;
WindowToCenterIn: HWND = 0): Boolean;
begin
with TAwCommonDialog.Create do
try
if WindowToCenterIn = 0 then
FCenterWnd := GetTopWindow
else
FCenterWnd := WindowToCenterIn;
FDialog := Dialog;
Result := Execute;
finally
Free;
end;
end;
{ TAwMessageBox }
type
TAwMessageBox = class(TObject)
private
FCaption: String;
FCenterWnd: HWND;
FFlags: Cardinal;
FHookProc: TFarProc;
FText: String;
FWndHook: HHOOK;
function Execute: Integer;
procedure HookProc(var Message: THookMessage);
end;
function TAwMessageBox.Execute: Integer;
begin
try
try
Application.NormalizeAllTopMosts;
FHookProc := MakeHookInstance(HookProc);
FWndHook := SetWindowsHookEx(WH_CALLWNDPROCRET, FHookProc, 0,
GetCurrentThreadID);
Result := Application.MessageBox(PChar(FText), PChar(FCaption), FFlags);
finally
if FWndHook <> 0 then
UnhookWindowsHookEx(FWndHook);
if FHookProc <> nil then
FreeHookInstance(FHookProc);
Application.RestoreTopMosts;
end;
except
Result := 0;
end;
end;
procedure TAwMessageBox.HookProc(var Message: THookMessage);
var
Data: PCWPRetStruct;
Title: array[0..255] of Char;
begin
with Message do
if nCode < 0 then
Result := CallNextHookEx(FWndHook, nCode, wParam, lParam)
else
Result := 0;
if Message.nCode = HC_ACTION then
begin
Data := PCWPRetStruct(Message.lParam);
if Data.message = WM_INITDIALOG then
begin
FillChar(Title, SizeOf(Title), 0);
GetWindowText(Data.hwnd, #Title, SizeOf(Title));
if String(Title) = FCaption then
begin
CenterWindow(FCenterWnd, Data.hwnd);
SetWindowPos(Data.hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE or
SWP_NOSIZE or SWP_NOACTIVATE or SWP_NOOWNERZORDER);
UnhookWindowsHookEx(FWndHook);
FWndHook := 0;
FreeHookInstance(FHookProc);
FHookProc := nil;
end;
end;
end;
end;
function MsgBox(const Text: String; Flags: Cardinal = DefFlags;
const Caption: String = DefCaption;
WindowToCenterIn: HWND = 0): Integer;
begin
with TAwMessageBox.Create do
try
if Caption = DefCaption then
FCaption := Application.Title
else
FCaption := Caption;
if WindowToCenterIn = 0 then
FCenterWnd := GetTopWindow
else
FCenterWnd := WindowToCenterIn;
FFlags := Flags;
FText := Text;
Result := Execute;
finally
Free;
end;
end;
end.
unit AwHookInstance;
interface
uses
Windows;
type
THookMessage = packed record
nCode: Integer;
wParam: WPARAM;
lParam: LPARAM;
Result: LRESULT;
end;
THookMethod = procedure(var Message: THookMessage) of object;
function MakeHookInstance(Method: THookMethod): Pointer;
procedure FreeHookInstance(HookInstance: Pointer);
implementation
const
InstanceCount = 313;
type
PHookInstance = ^THookInstance;
THookInstance = packed record
Code: Byte;
Offset: Integer;
case Integer of
0: (Next: PHookInstance);
1: (Method: THookMethod);
end;
PInstanceBlock = ^TInstanceBlock;
TInstanceBlock = packed record
Next: PInstanceBlock;
Code: array[1..2] of Byte;
HookProcPtr: Pointer;
Instances: array[0..InstanceCount] of THookInstance;
end;
var
InstBlockList: PInstanceBlock;
InstFreeList: PHookInstance;
function StdHookProc(nCode: Integer; wParam: WPARAM;
lParam: LPARAM): LRESULT; stdcall; assembler;
{ In ECX = Address of method pointer }
{ Out EAX = Result }
asm
XOR EAX,EAX
PUSH EAX
PUSH LParam
PUSH WParam
PUSH nCode
MOV EDX,ESP
MOV EAX,[ECX].Longint[4]
CALL [ECX].Pointer
ADD ESP,12
POP EAX
end;
function CalcJmpOffset(Src, Dest: Pointer): Longint;
begin
Result := Longint(Dest) - (Longint(Src) + 5);
end;
function MakeHookInstance(Method: THookMethod): Pointer;
const
BlockCode: array[1..2] of Byte = ($59 { POP ECX }, $E9 { JMP StdHookProc });
PageSize = 4096;
var
Block: PInstanceBlock;
Instance: PHookInstance;
begin
if InstFreeList = nil then
begin
Block := VirtualAlloc(nil, PageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
Block^.Next := InstBlockList;
Move(BlockCode, Block^.Code, SizeOf(BlockCode));
Block^.HookProcPtr := Pointer(CalcJmpOffset(#Block^.Code[2], #StdHookProc));
Instance := #Block^.Instances;
repeat
Instance^.Code := $E8; { CALL NEAR PTR Offset }
Instance^.Offset := CalcJmpOffset(Instance, #Block^.Code);
Instance^.Next := InstFreeList;
InstFreeList := Instance;
Inc(Longint(Instance), SizeOf(THookInstance));
until Longint(Instance) - Longint(Block) >= SizeOf(TInstanceBlock);
InstBlockList := Block;
end;
Result := InstFreeList;
Instance := InstFreeList;
InstFreeList := Instance^.Next;
Instance^.Method := Method;
end;
procedure FreeHookInstance(HookInstance: Pointer);
begin
if HookInstance <> nil then
begin
PHookInstance(HookInstance)^.Next := InstFreeList;
InstFreeList := HookInstance;
end;
end;
end.
Legal notice: These units are written by me in this Dutch topic. The original versions are from Mark van Renswoude, see NLDMessageBox.
Here's the code I currently use to show a centered dialog over the active form:
function MessageDlgCenter(const Msg: string; DlgType: TMsgDlgType;
Buttons: TMsgDlgButtons): Integer;
var R: TRect;
begin
if not Assigned(Screen.ActiveForm) then
begin
Result := MessageDlg(Msg, DlgType, Buttons, 0);
end else
begin
with CreateMessageDialog(Msg, DlgType, Buttons) do
try
GetWindowRect(Screen.ActiveForm.Handle, R);
Left := R.Left + ((R.Right - R.Left) div 2) - (Width div 2);
Top := R.Top + ((R.Bottom - R.Top) div 2) - (Height div 2);
Result := ShowModal;
finally
Free;
end;
end;
end;

Resources