or Long live Unicode!

The technology for loading images onto buttons was flawless for a long time, until two issues arose: the use of styles and screen scaling.

Since the background color of a button can vary greatly between different styles, even to the point of complete contrast, it is necessary to somehow change the color of the image for different styles. Unfortunately, My Visual Database does not have full support for working with the PNG format, so the only solution was to additionally store button images for those styles that contrast with the default style, and this is more than half of the dark styles. And, although a solution was found, from a practical point of view it is difficult to implement, especially if many different images are required.

But we couldn’t agree on scaling: My Visual Database has no means of controlling the size of images on buttons, other than loading images of the required size, and this is an even more expensive technology than creating buttons for each style. There are more and more high-resolution monitors, and more and more users are using application scaling. The revolutionary situation is obvious: something needs to be changed in technology.

On the My Visual Database developer forum, examples of the use of Unicode characters as images began to appear more and more often, support for which is fully implemented in new operating systems. Therefore, I decided to add a special parameter to the image management subsystem that will allow the use of Unicode characters on buttons using the same technology as images: just give the button a name for which an image is registered so that it automatically appears on the button.

Let me remind you that improvements are being made within the framework of the DataKeeper project.

Images.pas

Since all the improvements have been made to this module, I found it necessary to refresh the information about it. Moreover, the functionality of PNG images is quite relevant for most projects.

// Utilities for working with images
// 04.11.2022
// procedure Images_AddStyle(var ADir:string;); - adding “stylishness” to images
// procedure Images_ButtonsAssign( AIndex: integer; AWhiteList: string; ABlackList: string ); - assigning pictures to buttons on forms
// procedure Images_Init; - data initialization
// procedure Images_Load( AIndex:integer ); - loading images is done using a procedure with parameters, since the application may have several sizes of images that must be stored in different lists; pictures in png format
// procedure Images_Set( AButton:TdbButton; AImageName: string; AIndexOfSize: integer; AModel: integer ); - assign image index by file name
// procedure Images_SetButton( AButton:TdbButton); - simplified procedure for assigning an image to a button
// procedure Images_UCButtonsAssign; - assigning picture symbols to keys on forms

uses 'VClass\VCUtils.pas', 'VClass\Style.pas';

const
  IMAGES_UNICODE = True; // use UNICODE characters as images. Works well with scaling and styles.
  // folders are specified relative to the application folder
  IMAGES_UNICODE_INI_FILENAME = 'ucimages.ini'; // file with image-symbol settings
  IMAGES_UNICODE_RATIO = 0.8; // Unicode image size relative to button size
  IMAGES_DIR = 'Images\'; // folder where the images are located
  //
  IMAGES_SELECTED_SUFFIX = '_S'; // suffix in the name for the image of the selected button
  IMAGES_HOT_SUFFIX = '_H'; // suffix in the name for the button image when hovering the cursor
  IMAGES_DISABLE_SUFFIX = '_D'; // suffix in the name for the image of an inaccessible button
  IMAGES_PRESSED_SUFFIX = '_P'; // suffix in the name for the image of the pressed button
  // display models
  IMAGES_SIMPLE_MODEL = 1; // two images are used
  IMAGES_FULL_MODEL = 2; // four images are used
  // (!) configured for a specific project; see procedure Images_Init;
  IMAGES_FORMAT_COUNT = 1; // how many formats are in the library
  // list of formats
  IMAGES_FORMAT_BUTTON = 0; //
  IMAGES_FORMAT_BIGBUTTON = 1; //
  // align the image in the button
  iaLeft = 0;
  iaRight = 1;
  iaTop = 2;
  iaBottom = 3;
  iaCenter = 4;

var
   ImagesDir: array of string; // storage folders
   ImagesSize: array of integer; // image sizes
   ImagesList: array of TImageList; // image storage
   ImagesNames: array of TStringList; // names of loaded images
   ImagesUCNames: TStringList; // list for storing symbol names and displaying themCode language: Delphi (delphi)

The Images.pas module refers to the VCUtils.pas and Style.pas modules, which contain utilities for working with virtual classes and a library for working with styles.

The new IMAGES_UNICODE constant controls whether Unicode characters are used as button images.

procedure Images_Init;
// data initialization
var
   i:integer;
   tmpFileName: string;
begin
   if IMAGES_UNICODE then // if the pictures are UNICODE characters
   begin // then load the required file
     ImagesUCNames := TStringList.Create;
     tmpFileName := ExtractFilePath(Application.ExeName)+IMAGES_UNICODE_INI_FILENAME;
     if not FileExists(tmpFileName) then
       RaiseException('File not found ' + tmpFileName);
     ImagesUCNames.LoadFromFile(tmpFileName);
     Images_UCButtonsAssign;
   end
   else
   begin
     // initialization of data arrays
     SetLength(ImagesDir, IMAGES_FORMAT_COUNT);
     SetLength(ImagesSize, IMAGES_FORMAT_COUNT);
     SetLength(ImagesList, IMAGES_FORMAT_COUNT);
     SetLength(ImagesNames, IMAGES_FORMAT_COUNT);
     // (!) configuration performed for the project
     ImagesDir[IMAGES_FORMAT_BUTTON] := IMAGES_DIR + 'Buttons\';
     // style magic
     Style_AddImagesStyle( ImagesDir[IMAGES_FORMAT_BUTTON] );
     // size
     ImagesSize[IMAGES_FORMAT_BUTTON] := 24;
     {
     ImagesDir[IMAGES_FORMAT_BIGBUTTON] := IMAGES_DIR + 'BigButtons\';
     ImagesSize[IMAGES_FORMAT_BIGBUTTON] := 48;
     }
     // (!)
     // loading data
     for i := 0 to IMAGES_FORMAT_COUNT - 1 do
     begin
       ImagesNames[i] := TStringList.Create;
       ImagesList[i] := TImageList.Create(frmdbCoreAbout); // use the "About" form
       Images_Load(i); // load images
     end;
     // (!) Configured for the project
     Images_ButtonsAssign(IMAGES_FORMAT_BUTTON);
     // Images_ButtonsAssign( IMAGES_FORMAT_BIGBUTTON,'frmShow','' );
     // (!)
   end;
end;Code language: Delphi (delphi)

The initialization procedure causes the loading of resources, as well as a call to processing to install images on buttons on forms that exist at the time of launch. For Unicode, there is a separate new procedure, Images_UCButtonsAssign(), which has no parameters.

The ucimages.ini file contains <name>=<symbol> pairs. The first line should have a pair that is not used. This is due to the implementation of the TStrings.LoadFromFile() method and the UTF-8 text storage format.

UnicodeImages=
New=✚
Edit=✍
Delete=🗑
Save=✔
Cancel=✖
Search=🔎Code language: PHP (php)

Initializing PNG images still has architectural problems: if the project uses buttons of different sizes, then adding code to the body of the Images_Init() procedure is required. But the technology of using Unicode is more advanced in this sense, since the size of the “image” adapts to the size of the button inside the Images_Set() procedure.

procedure Images_UCButtonsAssign;
// assigning picture symbols to buttons on forms
var
   tmpForm: TAForm;
   tmpButton: TdbButton;
   i:integer;
   j:integer;
begin
   // for all application forms
   for i := 0 to Screen.FormCount - 1 do
   begin
     tmpForm := TAForm(Screen.Forms[i]);
     for j := 0 to tmpForm.ComponentCount - 1 do
     begin
       if tmpForm.Components[j] is TdbButton then
       begin
         tmpButton := TdbButton(tmpForm.Components[j]);
         Images_SetButton(tmpButton);
       end;
     end;
   end;
end;Code language: Delphi (delphi)

Initialization is simple: all the buttons are found, then the simplified image assignment procedure Images_SetButton() is called.

procedure Images_SetButton( AButton:TdbButton);
// simplified procedure for assigning an image to a button
begin
  Images_Set(AButton, DeleteSuffix(DeleteClassName(AButton.Name)), IMAGES_FORMAT_BUTTON, IMAGES_SIMPLE_MODEL);
end;
Code language: Delphi (delphi)

For compatibility, the universal procedure Images_Set() is used, which works with both Unicode and PNG images.

procedure Images_Set(AButton: TdbButton; AImageName: string; AIndexOfSize: integer; AModel: integer);
// assign image index by file name
// AButton:TdbButton; - button
// AImageName:string; - image name
// AIndexOfSize:integer; - size index
// AModel: integer - model
var
   tmpImageIndex: integer;
   tmpImageSelectedIndex: integer;
   tmpImageHotIndex: integer;
   tmpImageDisableIndex: integer;
   tmpImagePressedIndex: integer;
   tmpCaption: string;
begin
   AImageName := UpperCase(AImageName);
   if AImageName <> '' then
   begin
     if IMAGES_UNICODE then // if the pictures are UNICODE characters
     begin
       AButton.ImageIndex := -1;
       AButton.Images := nil;
       tmpCaption := AButton.Caption;
       AButton.Caption := ImagesUCNames.Values(AImageName);
       if tmpCaption <> '' then
       begin
         AButton.Caption := AButton.Caption + '' + tmpCaption;
       end
       else
       begin
         AButton.Font.Height := trunc(AButton.Height*IMAGES_UNICODE_RATIO);
       end;
     end
     else
     begin
       // main image
       tmpImageIndex := ImagesNames[AIndexOfSize].IndexOf(AImageName);
       tmpImageSelectedIndex := ImagesNames[AIndexOfSize].IndexOf(AImageName + IMAGES_SELECTED_SUFFIX);
       tmpImageHotIndex := ImagesNames[AIndexOfSize].IndexOf(AImageName + IMAGES_HOT_SUFFIX);
       tmpImageDisableIndex := ImagesNames[AIndexOfSize].IndexOf(AImageName + IMAGES_DISABLE_SUFFIX);
       tmpImagePressedIndex := ImagesNames[AIndexOfSize].IndexOf(AImageName + IMAGES_PRESSED_SUFFIX);
       if tmpImageIndex >= 0 then
       begin
         AButton.Images := ImagesList[AIndexOfSize];
         AButton.ImageIndex := tmpImageIndex;
         case AModel of
           IMAGES_SIMPLE_MODEL:
             begin
               AButton.SelectedImageIndex := tmpImageSelectedIndex;
               AButton.HotImageIndex := tmpImageSelectedIndex;
               AButton.DisabledImageIndex := tmpImageIndex;
               AButton.PressedImageIndex := tmpImageSelectedIndex;
             end;
           IMAGES_FULL_MODEL:
             begin
               AButton.SelectedImageIndex := tmpImageSelectedIndex;
               AButton.HotImageIndex := tmpImageHotIndex;
               AButton.DisabledImageIndex := tmpImageDisableIndex;
               AButton.PressedImageIndex := tmpImagePressedIndex;
             end;
         end;
       end;
     end;
   end;
end;
Code language: Delphi (delphi)

If there is an inscription on the button, then a picture symbol is added before the inscription. If there is no inscription, then the size of the picture symbol changes in accordance with the proportion to the button size, which is specified by the IMAGES_UNICODE_RATIO constant.

For a complete overview of the Images.pas module, below is the procedure for loading images from folders and the procedure for initializing buttons with PNG images.

procedure Images_Load(AIndex: integer);
// loading of pictures is done by a procedure with parameters, since the application may have several sizes of images that must be stored in different lists; pictures in png format
var
   tmpList: TStringList; // list of files in the folder with pictures
   tmpImageList: TImageList;
   tmpImageNames: TStringList;
   i:integer;
   tmpImageDir: string;
   s:string;
begin
   tmpImageDir := ExtractFilePath(Application.ExeName) + ImagesDir[AIndex];
   if DirectoryExists(tmpImageDir) then
   begin
     tmpImageList := ImagesList[AIndex];
     tmpImageNames := ImagesNames[AIndex];
     //
     tmpImageList.Masked := false;
     tmpImageList.ColorDepth := cd32bit;
     // size of pictures
     tmpImageList.Width := ImagesSize[AIndex];
     tmpImageList.Height := ImagesSize[AIndex];
     tmpList := TStringList.Create;
     try
       // take only files from the specified folder
       // images for styles can be stored in subfolders
       tmpList.Text := GetFilesList(tmpImageDir, '*.*', false);
       for i := 0 to tmpList.Count - 1 do
       begin
         tmpImageList.AddPng(tmpList.strings[i]);
         s := UpperCase(ExtractFileName(tmpList.strings[i]));
         s := copy(s, 1, Length(s) - 4);
         tmpImageNames.Add(s);
       end;
     finally
       tmpList.Free;
     end;
   end
   else
     RaiseException('Folder not found ' + tmpImageDir);
end;

procedure Images_ButtonsAssign(AIndex: integer; AWhiteList: string = ''; ABlackList: string = '');
// assigning images to buttons on forms
// assignment goes by name, class name and suffix are removed
// AIndex - image format
// AWhiteList - white list, change only specified forms
// ABlackList - black list, exclude forms
var
   tmpForm: TAForm;
   tmpButton: TdbButton;
   tmpName: string;
   i:integer;
   j:integer;
   tmpByName: boolean;
   tmpFormList: string;
begin
   // add separators (commas) to the white and black lists so that the search is correct
   if AWhiteList <> '' then
   begin
     tmpByName := True;
     tmpFormList := ',' + AWhiteList + ',';
   end
   else
   begin
     tmpByName := false;
     tmpFormList := ',' + ABlackList + ',';
   end;
   // for all application forms
   for i := 0 to Screen.FormCount - 1 do
   begin
     tmpForm := TAForm(Screen.Forms[i]);
     // check if the form is included in the black or white list
     if (tmpByName and (pos(tmpForm.name, tmpFormList) > 0)) or (not tmpByName and (pos(tmpForm.name, tmpFormList) = 0))
     then
       for j := 0 to tmpForm.ComponentCount - 1 do
       begin
         if tmpForm.Components[j] is TdbButton then
         begin
           tmpButton := TdbButton(tmpForm.Components[j]);
           tmpName := DeleteSuffix(DeleteClassName(tmpButton.name));
           Images_Set(tmpButton, tmpName, AIndex, IMAGES_SIMPLE_MODEL);
         end;
       end;
   end;
end;Code language: Delphi (delphi)

Scaling

To automatically scale forms, just set the Scaled = True property for forms created in the My Visual Database designer. And for elements created by program code, we had to add the Scale() function, which is used wherever there were previously constants defining the coordinates or size of components.

const
   DEFAULT_DPI = 96; // standard screen resolution

var
   ScaleRatio: double; // scaling factor;

function Scale(AValue:integer):integer;
// scaling
begin
   Result := Trunc( AValue * ScaleRatio );
end;

function ScaleFontSize: integer;
// scaled and standard font size for this application
begin
   Result := frmMain.tgrStyle.Font.Size;
end;

function GetDesktopDPI: integer;
// getting the current screen resolution.
// default screen resolution is 96 DPI
var
   tmpReg:TRegistry;
begin
   tmpReg := TRegistry.Create;
   tmpReg.RootKey := HKEY_CURRENT_USER;
   tmpReg.Access := KEY_READ;
   if tmpReg.OpenKey('Control Panel\Desktop\WindowMetrics', False) then
     Result := tmpReg.ReadInteger('AppliedDPI')
   else
     Result := DEFAULT_DPI;
   tmpReg.CloseKey;
   tmpReg.Free;
end;Code language: Delphi (delphi)

At first I wanted to determine the screen DPI and adjust the scale to it. But in practice it turned out that when changing the scale, the DPI does not change. Then I did it easier. The main form has a tgrStyle component, which I use to style the table grids I create. Knowing the original size of this element and the size that resulted from standard scaling, I get the scaling factor.

ScaleRatio := frmMain.tgrStyle.Width / 500;

Result

Unicode pictures in light theme
Unicode pictures in dark theme
Maintaining the proportions of pictures when changing the display scale of applications.

Links

One thought on “Image revolution”
  1. Hello, I think your site might be having browser compatibility issues. When I look at your blog site in Firefox, it looks fine but when opening in Internet Explorer, it has some overlapping. I just wanted to give you a quick heads up! Other then that, awesome blog!

Leave a Reply

Your email address will not be published. Required fields are marked *