Root > How to... > ...change exception dialog? > ...modify WinAPI dialog?

...modify WinAPI dialog?

Previous pageReturn to chapter overviewNext page   

This article is continuation of How to change exception dialog? Please, read How to change exception dialog? first.

 

EurekaLog dialogs are WinAPI-based, which means they are using functions like CreateDialog to create HWND from resource template. Therefore, if you want to modify the dialog - you need to create your own dialog template, include it in resources, then load modified template instead of default one.

 

First, you need a resource editor application. Examples below will use Restorator application, but there are many other resource editor applications for Windows. You may use any editor you like.

 

Alternatively, you may also use any text editor (like Notepad) to edit *.rc files manually, and then compile *.rc files into *.res files using RAD Studio's resource compiler or IDE's capabilities. This is possible, but not very convenient. Resource editor applications allow you to edit dialog windows visually, provide syntax checking, show preview, etc.

 


 

First, you need to create a new blank resource file (*.res), which will hold a modified template for the dialog.

 

Second, you need to add a new dialog template:

 

 

Adding a new dialog template into *.res file

 

Template's name can be arbitrary string of your choice. Enter some name which will allow you to identify this dialog template in your code later (see below).

 

If you are not using resource editor application and edit *.rc files instead - use DIALOGEX resource type.

 

Important Notes:

Make sure that you are adding language-neutral resource;
Make sure that template name uses UPPER CASE only.

 

Since you don't want to create a new WinAPI dialog from scratch, but rather modify an existing EurekaLog's dialog - you need to copy predefined template from EurekaLog into your freshly created dialog template. You may find EurekaLog's dialog templates inside \Lib\Common\ folder of your EurekaLog installation (which is C:\Program Files (x86)\Neos Eureka S.r.l\EurekaLog 7\Lib\Common\ by default). Open UnicodeDialog.res file (for Delphi 2009 and above) or Dialog.res file (for Delphi 2007 and below), then open a corresponding dialog template (EL_MS_DIALOG for MS Classic style, EL_DIALOG for EurekaLog style, etc.):

 

 

Copying dialog template from EurekaLog into your *.res file

 

Copy EurekaLog's dialog template resource and paste it into your dialog template.

 

Now, create your modified dialog class:

 

uses
  EDialog,                // for RegisterDialogClassFirst
  EDialogWinAPIEurekaLog; // for TEurekaLogDialog
 
type
  // Use same dialog name, because we want to alter existing dialog
  TEurekaLogDialog = class(EDialogWinAPIEurekaLog.TEurekaLogDialog)
  protected
    // Returns name of dialog template in resources
    function GetDialogName: Stringoverride;
  end;
 
// Embed your *.res file inside application
{$R MyCustomDialogTemplate.res}
 
{ TEurekaLogDialog }
 
function TEurekaLogDialog.GetDialogName: String;
begin

  // Dialog name in resources, MUST BE UPPERCASE!
  Result := 'MY_MODIFIED_DIALOG'
end;
 
initialization
  // Register dialog class to be the first in the list.
  RegisterDialogClassFirst(TEurekaLogDialog);
end.

 

(Replace EDialogWinAPIEurekaLog unit name and TEurekaLogDialog class name with names of dialog that you want to modify)

 

Now EurekaLog's dialog (which is EurekaLog style in the example above) will use your customizations.

 

Note: we did not make any actual modifications yet. We just created a customized dialog class that is currently equal to default one. See below for an example of actual customization.

 


 

Now let's add an edit box with button and label to EurekaLog's dialog. This example assumes that you have performed preparatory steps as shown above.

 

Open your dialog template and add new controls:

 

 

Adding new controls to the dialog

 

Please, refer to Resource-Definition Statements for list of possible controls, as well as proper syntax for each control type.

 

Example above adds three new controls:

A label (CTEXT) with ID of 20'000;
An edit box (EDITTEXT) with ID of 20'001;
A button (PUSHBUTTON) with ID of 20'002.

 

IDs are arbitrary 16-bit integers. Those are used to identify your controls in code (see below). We recommend to use values like 20'xyz (e.g. 20'001, 20'002, etc.) to avoid collision with IDs defined by EurekaLog.

 

The remaining numbers in controls' descriptions are coordinates and styles.

 

Note: the exact control's position and size do not matter that much, because position is adjusted based on current DPI and dialog layout (options) manually in dialog's code (see below). You may enter approximate coordinates to preview dialog.

 

It is a good idea to duplicate your new IDs as constants in your code:

 

// ...
 
{$R MyCustomDialogTemplate.res}
 
const

  // New label ID  - as specified in your resource template
  ID_MY_LABEL  = 20000; 

  // New edit ID   - as specified in your resource template
  ID_MY_EDIT   = 20001; 

  // New button ID - as specified in your resource template
  ID_MY_BUTTON = 20002; 
 
{ TEurekaLogDialog }
 
// ...

 

You will also need to add your controls to default code for style bookkeeping:

 

// ...
 
type
  TEurekaLogDialog = class(EDialogWinAPIEurekaLog.TEurekaLogDialog)
  protected
    // ...
 
    // Assigns dynamic fonts from Windows settings to controls
    procedure SetFonts; override;
    // Adjusts dialog controls to Right-To-Left style
    procedure SetRTLStyle; override;
  end;
 
{ TEurekaLogDialog }
 
// ...
 
procedure TEurekaLogDialog.SetFonts;
begin
  inherited;
 
  // VariableFont is a default font for dialog's content.
  // It is already adjusted for DPI and user settings.
  // Additional fonts are available:
  // - CaptionFont
  // - FixedFont
  // - LinkFont
  // - BoldFont
  SendMessage(GetDlgItem(Dialog, ID_MY_LABEL),
    WM_SETFONT, LPARAM(VariableFont), 1);
  SendMessage(GetDlgItem(Dialog, ID_MY_EDIT),
    WM_SETFONT, LPARAM(VariableFont), 1);
  SendMessage(GetDlgItem(Dialog, ID_MY_BUTTON),
    WM_SETFONT, LPARAM(VariableFont), 1);
end;
 
procedure TEurekaLogDialog.SetRTLStyle;
begin
  inherited;
 
  // Nothing to do for default Left-To-Right dialogs
  if not RTLDialog then
    Exit;
 
  // Reverse controls for Right-To-Left dialogs:
  MirrorWnd(GetDlgItem(Dialog, ID_MY_LABEL));
  MirrorWnd(GetDlgItem(Dialog, ID_MY_EDIT));
  MirrorWnd(GetDlgItem(Dialog, ID_MY_BUTTON));
end;
 
// ...

 

Next, you need to arrange your new controls on dialog. You can do this by overriding:

 

function WindowAlign(const ADPI: Integer;
  const APosition: TRect;
  const AFit: Boolean): TRect; override;

 

This method is used like this:

1. When window is initializing (e.g. first time call): WindowAlign is called with ADPI = 0 and AFit = False. APosition contains desired default position of window in screen coordinates (usually something like 500x500 pixels). WindowAlign must arrange controls inside that area. If area is not large enough to fit all enabled controls (or area is too large) - WindowAlign must resize window to target size and call itself again (with AFit = True and new adjusted APosition);
2. When dialog is being resized by end user (for sizeable windows only): WindowAlign is called with ADPI = 0 and AFit = True. APosition contains new position of window;
3. When DPI of the monitor is changed: WindowAlign is called with ADPI <> 0 and AFit = False;
4. When WindowAlign is called with AFit = True: it must arrange all controls inside window without changing its size.

 

For example, if we want to achieve the following window layout:

 

 

An example of custom layout

(custom controls are highlighted)

 

Then we can use this sample implementation:

 

// ...
 
type
  TEurekaLogDialog = class(EDialogWinAPIEurekaLog.TEurekaLogDialog)
  protected
    // ...
 
    // Arranges controls inside dialog
    function WindowAlign(const ADPI: Integer;
      const APosition: TRect;
      const AFit: Boolean): TRect; override;
 
    // ...
  end;
 
// ...
 
function TEurekaLogDialog.WindowAlign
  (const ADPI: Integer;
   const APosition: TRect;
   const AFit: Boolean): TRect;
 
  // Get (client) coordinates of the control
  function GetControlClientSize(const AID: Cardinal): TRect;
  begin
    // Get screen coordinates
    if GetWindowRect(GetDlgItem(Dialog, AID), Result) then
    begin
      // Convert screen coords to dialog's client coords
      ScreenToClient(Dialog, Result.TopLeft);
      ScreenToClient(Dialog, Result.BottomRight);
    end
    else
    begin
      // This should not really happen
      FillChar(Result, SizeOf(Result), 0);
      Result.Top := 0;
      Result.Left := 0;
      Result.Right := Result.Left + 1;
      Result.Bottom := Result.Top + 1;
    end;
  end;
 
const
  // Default spacing between controls, pixels on 96 DPI
  DefSpace = 8;
 
var
  // DefSpace adjusted to current ADPI
  ScaledDefSpace: Integer;
  // Text height in pixels, DPI adjusted
  TextHeight: Integer;
  // Edit height in pixels, DPI adjusted
  EditHeight: Integer;
  // Button size in pixels, DPI adjusted
  ButtonWidth: Integer;
  ButtonHeight: Integer;
  // Vertical space in pixels occupied by our controls
  Delta: Integer;
  // Height of window's borders and caption,
  // e.g. difference between client area and rect
  BordersHeight: Integer;
 
  Text: String;
  R: TRect;
  LeftX: Integer;
  TopY: Integer;
  MaxRight: Integer;
  NewWndHeight: Integer;
  NewWndClientHeight: Integer;
begin
  // Result is non-client rectangle of window,
  Result := inherited WindowAlign(ADPI, APosition, AFit);
  // Window is not initialized yet?
  if Icon = 0 then
    Exit;
 
  // You can check if you need to display your controls:
  // This is just an example!
  if not (
     // You can check options if you want
     Options.CustomFieldBool['Custom_My_Option'and
     // You can also check properties
     ShowSendErrorControl) then
  begin
    // Hide your controls
    ShowWindow(GetDlgItem(Dialog, ID_MY_LABEL),  SW_HIDE);
    ShowWindow(GetDlgItem(Dialog, ID_MY_EDIT),   SW_HIDE);
    ShowWindow(GetDlgItem(Dialog, ID_MY_BUTTON), SW_HIDE);
    Exit;
  end;
 
  // Show your controls
  ShowWindow(GetDlgItem(Dialog, ID_MY_LABEL),  SW_SHOW);
  ShowWindow(GetDlgItem(Dialog, ID_MY_EDIT),   SW_SHOW);
  ShowWindow(GetDlgItem(Dialog, ID_MY_BUTTON), SW_SHOW);
 
  // We are good to go
  // We need to arrange our controls on window
 
  // Determinate sizes for various things
  ScaledDefSpace := ScaleSize(DefSpace);
  TextHeight := GetTextHeight(VariableFont,
    'SomeRandomText WqQ', WndClientWidth);
  EditHeight := TextHeight +
    GetSystemMetricsForDPI(SM_CYBORDER, DPI) * 4;
  ButtonHeight := TextHeight + ScaleSize(6) +
    GetSystemMetricsForDPI(SM_CYBORDER, DPI) * 4;
  BordersHeight := WndHeight - WndClientHeight;
 
  // Set captions:
  Text := Options.CustomizedExpandedTexts
    [mtMSDialog_HowToReproduceCaption];
  SendMessage(GetDlgItem(Dialog, ID_MY_LABEL), WM_SETTEXT, 0,
    // From localized messages:
    LPARAM(PChar(Text)));
  SendMessage(GetDlgItem(Dialog, ID_MY_EDIT), WM_SETTEXT, 0,
    // From properties:
    LPARAM(PChar(ReproduceText)));
  SendMessage(GetDlgItem(Dialog, ID_MY_BUTTON), WM_SETTEXT, 0,
    // From contants
    LPARAM(PChar('Save report to file')));
 
  // Calculate additional vertical space for window
  Delta := ScaledDefSpace + TextHeight +
           ScaledDefSpace + EditHeight;
  if AFit then
  begin
    // AFit = True:
    // We need to fit controls into existing window's area
 
    // We start to place our controls at the bottom of the window
    TopY := WndClientHeight - Delta;
 
    // Inherited WindowAlign shrinked area, enlarge it back
    Result.Bottom := Result.Bottom + Delta;
  end
  else
  begin
    // AFit = False:
    // We need to adjust window's area for controls
 
    // We start to place our controls at the bottom of the window
    NewWndHeight := Result.Bottom - Result.Top;
    NewWndClientHeight := NewWndHeight - BordersHeight;
    TopY := NewWndClientHeight;
  end;
 
  // Determinate how much space is occupied by "left" buttons
  if Options.edoShowCustomButton then
  begin
    // Custom button is last button
    // (in "left buttons" group)
    R := GetControlClientSize(ID_CUSTOM);
    MaxRight := R.Right + ScaledDefSpace;
  end
  else
  if (GetWindowLong(
        GetDlgItem(Dialog, ID_TERMINATE),
        GWL_STYLE) and
      WS_VISIBLE) <> 0 then
  begin
    // Terminate / restart button is last button
    // (in "left buttons" group)
    R := GetControlClientSize(ID_TERMINATE);
    MaxRight := R.Right + ScaledDefSpace;
  end
  else
    // No buttons on the left
    MaxRight := ScaledDefSpace;
 
  R := GetControlClientSize(ID_BUTTON);
  // Place our button to the left of default button, same row
  ButtonWidth := ScaleSize(16) +
    GetTextWidth(VariableFont, 'Save report to file');
  LeftX := R.Left - ScaledDefSpace - ButtonWidth;
 
  // Our button overlaps left group of buttons?
  if (not AFit) and
     (LeftX < MaxRight) then
  begin
    // Enlarge horizontal space
    Result.Right := Result.Right + (MaxRight - LeftX + 1) * 2;
    SetWindowPos(Dialog, 0, 0, 0,
      Result.Right - Result.Left,
      Result.Bottom - Result.Top,
      SWP_NOMOVE or SWP_NOREPOSITION or
      SWP_NOREDRAW or SWP_NOSENDCHANGING or
      SWP_NOZORDER);
    GetSizes(Result);
    Result := WindowAlign(ADPI, Result, False);
    Exit;
  end;
 
  // Position our controls:
  SetWindowPos(GetDlgItem(Dialog, ID_MY_LABEL),  0,
    ScaledDefSpace, TopY,
    GetTextWidth(VariableFont, Text), TextHeight + 1,
    SWP_NOZORDER or SWP_NOOWNERZORDER or SWP_NOREDRAW);
  TopY := TopY + TextHeight + ScaledDefSpace;
 
  SetWindowPos(GetDlgItem(Dialog, ID_MY_EDIT),   0,
    ScaledDefSpace, TopY,
    WndClientWidth - ScaledDefSpace * 2, EditHeight,
    SWP_NOZORDER or SWP_NOOWNERZORDER or SWP_NOREDRAW);
  TopY := TopY + EditHeight + ScaledDefSpace;
 
  SetWindowPos(GetDlgItem(Dialog, ID_MY_BUTTON), 0,
    LeftX, R.Top,
    ButtonWidth, ButtonHeight,
    SWP_NOZORDER or SWP_NOOWNERZORDER or SWP_NOREDRAW);
  // this does not change height,
  // button will be inline with other buttons
 
  if AFit then
  begin
    // Repaint window when DPI changes:
    if ADPI <> 0 then
      InvalidateRect(Dialog, nil, True);
  end
  else
  begin
    // Resize dialog window:
    Result.Bottom := Result.Bottom + Delta;
    SetWindowPos(Dialog, 0, 0, 0,
      Result.Right - Result.Left,
      Result.Bottom - Result.Top,
      SWP_NOMOVE or SWP_NOREPOSITION or
      SWP_NOREDRAW or SWP_NOSENDCHANGING or
      SWP_NOZORDER);
    GetSizes(Result);
    Result := WindowAlign(ADPI, Result, True);
    Exit;
  end;
end;
 
// ...

 

This is quite a complex code, because it needs to integrate your controls right between already set EurekaLog's controls.

 

Finally, you need to add behavior for your controls. For example, if you want to add reaction for a button:

 

// ...
 
type
  TEurekaLogDialog = class(EDialogWinAPIEurekaLog.TEurekaLogDialog)
  protected
    // ...
 
    // To handle commands
    function DialogProc(const Msg: UINT;
      const _wParam: WPARAM;
      const _lParam: LPARAM): LRESULT; override;
 
    // ...
  end;
 
// ...
 
function TEurekaLogDialog.DialogProc
  (const Msg: UINT; const _wParam: WPARAM;
   const _lParam: LPARAM): LRESULT;
 
  procedure SaveReportToFile;
  var
    X: Integer;
  begin
    // Generate full bug report
    // (with screenshots, attaches, etc.)
    PrepareFilesForSend;
 
    // Copy all files attachments to some folder.
    // Of course, folder must exist and be writable.
    // Usually there is only one file (.elp),
    // but it can be few files -
    // depends on your project options
    for X := 0 to FilesToSend.Count - 1 do
      CopyFile(PChar(FilesToSend[X]),
        // You may also ask user for 

        // file/folder name with dialog
        PChar('N:\BugReports\' +
        ExtractFileName(FilesToSend[X])), False);
  end;
 
begin
  Result := inherited DialogProc(Msg, _wParam, _lParam);
 
  case Msg of
    WM_COMMAND: // "Button clicked"
    begin
      case _wParam of
        // Our custom "Save report to file" button
        ID_MY_BUTTON:
        begin
          SaveReportToFile;
 
          // Button closes dialog
          Close;
 
          // Indicate that message was handled
          Result := 1;
        end;
      end// case _wParam of
    end;
  end// case Msg of
end;
 
// ...

 

Additionally, if you need to perform various actions depending on controls state when dialog closes:

 

// ...
 
type
  TEurekaLogDialog = class(EDialogWinAPIEurekaLog.TEurekaLogDialog)
  protected
    // ...
 
    // Apply non-command controls when dialog closes
    function AssignOnCloseData: Boolean; override;
 
    // ...
  end;
 
// ...
 
function TEurekaLogDialog.AssignOnCloseData: Boolean;
var
  ReproduceSteps: String;
begin
  // Note that this also called
  // when EurekaLog switches dialogs
  // You can check if it is an actual close:
  // if FResponse.SendResult <> srRestart then
 
  // Must be performed before calling inherited
  // Because inherited will show pop up dialog
  // asking for steps to reproduce
  // (if no steps are set)
  ReproduceSteps := GetEditText(ID_MY_EDIT);
  SetReproduceText(ReproduceSteps);
 
  // Returns True to close dialog,
  // False to ignore
  Result := inherited AssignOnCloseData;
 
  // Here you can check your checkboxes (if any)
  // You can manipulate FResponse
  // And you can assign .Options back
end;
 
// ...

 

If you want to insert user's input from your controls into bug report - see this article (scroll down to "Adding user input from custom dialog" section).

 

 

See also:




Send feedback... Build date: 2021-10-15
Last edited: 2020-05-20
PRIVACY STATEMENT
The documentation team uses the feedback submitted to improve the EurekaLog documentation. We do not use your e-mail address for any other purpose. We will remove your e-mail address from our system after the issue you are reporting has been resolved. While we are working to resolve this issue, we may send you an e-mail message to request more information about your feedback. After the issues have been addressed, we may send you an email message to let you know that your feedback has been addressed.


Permanent link to this article: https://www.eurekalog.com/help/eurekalog/how_to_modify_winapi_dialog.php