Performance issue with TImageList

Performanceissue with TImageList

Update: This problem is solved with Delphi 10.1 Berlin

(Deutsche übersetzung im nachfolgenden Blog Eintrag)

Description of the problem:

Adding pictures to a Imagelist within a VCL application is very slow after a the change to Delphi 10 Seattle.

Usually adding pictures is very fast – as long as you act like this:

... 
iml: TImageList; 
...  
iml.BeginUpdate; 
try   
  for x in BigList do begin     
    iml.Add(x);
  end; 
finally   
  iml.EndUpdate; 
end;

One reason for a long time to add many pictures is, that clients of the imagelist get informed about every change on the list. To send this information just once and just at the end of all inserts we use BeginUpdate and EndUpdate.

This BeginUpdate and EndUpdate does not work like it should with Delphi 10 Seattle.

To understand this we look at the source coude:

Inheritance ot the TImageList:

Base is TBaseImageList in System.ImageList.

For FMX in Unit FMX.ImgList we have TCustomImageList >> TImageList

For VCL according in Unit VCL.ImgList: TCustomImageList >> TImageList

The methods BeginUpdate and EndUpdate in the base class looks like this:

 
procedure TBaseImageList.BeginUpdate; 
begin   
  if FUpdateCount = 0 then     
    Updating;   
    Inc(FUpdateCount); 
  end;  

procedure TBaseImageList.EndUpdate; 
begin   
  if FUpdateCount > 0 then begin
     Dec(FUpdateCount);
     if FUpdateCount = 0 then
       Updated;   
     end;
 end;

Both methods does what we expect.

Unfortunately the class TCustomImageList overwrite this for VCL (not override, not virtual!):

 
procedure TCustomImageList.BeginUpdate;
 begin
   Inc(FUpdateCount);
 end;

procedure TCustomImageList.EndUpdate;
begin   
  if FUpdateCount > 0 then 
    Dec(FUpdateCount);
  if FChanged then begin     
    FChanged := False;     
    Change;
  end;
end;

Updateing and Updated is not set anymore. UpdateCount is reintroduced and is not used further. The change method of the base class will call DoChange on every add – our BeginUpdate does not have effect.

Comparison to older sources of the VCL show that something went wrong here.

The solution of this problem indeed is easy: We just use the „good“ method of the base class.

Only problem: You have to check on further delphi versions, if there are may changes in the VCL method, which we do not want to skip, cause we do not execute them from now on.

I think this dephi bug will be solved in future – so we can go back to the original code.

...
iml: TImageList;
...
TBaseImageList(iml).BeginUpdate;
try
   for x in BigList do begin
     iml.Add(x);
   end;
 finally
   TBaseImageList(iml).EndUpdate;
 end;

Performanceproblem bei TImageList

Performanceproblem bei der TImageList

Update: Dieses Problem wurde mit Delphi 10.1 Berlin gelöst.

Problembeschreibung:

Das Hinzufügen von Bildern zu einer Imageliste in einer VCL Anwendung dauert nach der Umstellung auf Delphi 10 Seattle plötzlich sehr lange.

Normalerweise geht das hinzufügen von Bildern sehr schnell – vorausgesetzt, man befolgt in etwa diesen Ablauf:

  ...
  iml: TImageList;
  ...
  iml.BeginUpdate;
  try
    for x in BigList do
      begin
        iml.Add(x);
      end;
  finally
    iml.EndUpdate;
  end;

Wenn dieser Vorgang nun plötzlich sehr lange dauert kann es folgenden Grund haben:

Elemente, die diese Imageliste verwenden, werden über Änderungen an der Liste informiert.

Damit bei vielen Änderungen diese Benachrichtigung nur und erst am Ende der Aktualisierungen versendet wird, wird ein BeginUpdate/EndUpdate verwendet.

Dieses BeginUpdate und EndUpdate funktioniert in Delphi 10 Seattle jedoch nicht wie gewünscht.

Erkenntnisse liefert hier ein Blick auf den Quellcode:

 

Die Vererbungshierarchie von TImageList:

Basis ist TBaseImageList in System.ImageList.

Für FMX dann in Unit FMX.ImgList: TCustomImageList >> TImageList

Für VCL entsrpechend in Unit VCL.ImgList: TCustomImageList >> TImageList

 

Das BeginUpdate und EndUpdate in der Basisklasse sieht wie folgt aus:

  procedure TBaseImageList.BeginUpdate;
  begin
    if FUpdateCount = 0 then
      Updating;
    Inc(FUpdateCount);
  end;
  
  procedure TBaseImageList.EndUpdate;
  begin
    if FUpdateCount >
      0 then begin Dec(FUpdateCount);
    if FUpdateCount = 0 then
      Updated;
  end;

Diese beiden Methoden machen exakt was sie sollen.

Unglücklicherweise werden beide Methoden jedoch von TCustomImageList für die VCL überschrieben (nicht vererbt!):

  procedure TCustomImageList.BeginUpdate;
  begin
    Inc(FUpdateCount);
  end;

  procedure TCustomImageList.EndUpdate;
  begin
    if FUpdateCount >
      0 then Dec(FUpdateCount);
    if FChanged then
      begin
        FChanged := False;
        Change;
      end;
  end;

Hier wird das Updating und Updated nicht mehr gesetzt – und der UpdateCount, der noch dazu lokal in der Klasse deklariert ist und das geerbte UpdateCount überschreibt – wird nicht verwendet.

Die Methode TBaseImageList.Change der Basisklasse wird immer alle eingetragenen Klassen über die geänderte Imageliste informieren.

 

Ein Vergleich mit älteren Delphi VCL Quellen zeigt, dass hier etwas falsch läuft und so nicht funktioniert.

 

Die Lösung für das Problem ist als Workaroud jedoch recht einfach: Es wird einfach die „gute“ Methode der Basisklasse gerufen.

Einziger Problempunkt: Es müsste bei kommenden Delphi-Updates kontrolliert werden, ob sich zusätzliche Änderungen in der Ableitung ergeben haben, welche aktuell natürlich nicht ausgeführt werden.

Es ist anzunehmen, dass dieser Bug in Zukunft behoben wird. Dann kann diese Stelle wieder auf das Orginal geändert werden.

 

Durchgeführte Änderung: Aufruf der Methoden der Basisklasse:

  ...
  iml: TImageList;
  ...
  TBaseImageList(iml).BeginUpdate;
  try
    for x in BigList do
      begin
        iml.Add(x);
      end;
  finally
    TBaseImageList(iml).EndUpdate;
  end;

SQLite ANSI UTF8

SQLite Character Encodings

Converting from ANSI to UTF8

Introduction

SQLite is a pretty good database. It does what you want – which is not always given with other databases.
Unfortunately what you want is not always that what you really do. This is quite often found with the character encoding within the database.
SQLite is quite simple: It works in UTF8 char mode (as long as you do not explicit switch to UTF16). What the database never does is store strings in native ANSI mode. This is quite good – because it is definitly not up do date.
The problem of some programmers is that they just stored their data as ANSI in a UTF8 database. As long as they are in their own world this is not a problem. The problem occur, if you either want to use the database with other programs which expect the data in the correct UTF8 encoding or if you want to port your application and the new database driver only can handle the data as UTF8.

This situation occurs quite often within the Embarcadero RAD Studio (Delphi/C++) environment. Versions below Delphi 2009 handled string by default as ansistring. Since Version 2009 everything is handled by default as unicode string.
If you port your application to a new version you recognize, that your data are not those you expect.

Detect the problem

How can you find out in what format your data are stored?
You only see this if you have some chars in your database, which are different encoded in ANSI and UTF8.
All characters with bytecode 0..127 are just the same. If you have no other chars in your database you definitly have not problem: There is no difference with the encoding – so you have nothing to do.
All Chars between code 128..255 are coded as 2-4 byte code in UTF8.
The ANSI encoded data between 128..255 are encoded according to the codepage you use – often it uses „ISO Latin 1 (ISO 8859-1)“ standard.
Sample:
This is what we want to see: sqlite-decoding-is-ok
If you have a UTF8 encoded char in your database and you access this as ANSI, you will see 2 ANSI byte.
The sample above will be shown as utf8-special-char-decoded-as-ansi.
If you have a ANSI encoded char above charcode 128 in your database and you access this as UTF8, you will see a <?> by most applications due it is a non valid UTF8 char: ansi-special-char-decoded-as-utf8

For details about this see some tables of some encodings: http://www.utf8-zeichentabelle.de/ and the Wikipedia UTF8 article.
A quite good program to detect your encoding and simple switch the view to your data is Sqlite Expert.
In the options of the program you can select in which encoding you want your data to be displayed.

SqliteExpertAnsiUtf8Option

Convert the data

To convert the data from the „wrong“ ANSI format to the native Sqlite3 format UTF8 you have different options. But: Some of them are may not simple – and some drivers just refuse e.g. to encode the data in a different encoding than the database want.
We had problems to use newer Zeos versions or the FireDAC (formerly AnyDAC) Database drivers cause they always assume that you store UTF8 encoded strings and you have to use binary access or strange tricks to do the work there.
This method we show here works finally and is also very simple to use and to understand.
We use Delphi 2007 (which work by default with non unicode strings) and the Zeos Library in Version V7.0.0 (SVN revision 940). The Zeos library is freeware and can be used for all delphi versions as well as for Lazarus.
We also want that our application, which used ANSI encoding so far, convert the data after the startup and work from now on with the UTF8 encoding.
To detect, if we have already decoded we save a flag in the database with the information if we are already in UTF8 mode.

 
  procedure TdmBase.ConvertUtf8P;
  // convert all defined fields from ansi to utf8
  var
    iIdx: integer;
  begin // detect if we need to convert
    if not f_InternalCheckOkF(cdUtf8) or (i_InternalGetF(cdUtf8) = 0) then
      begin
        // convert all tables, which may contain ansi>128 chars
        for iIdx := 0 to Length(axUtfFields) - 1 do
          begin
            if not axUtfFields[iIdx].fDisplayOnly then
              begin
                iUtf8CurrentIdx := iIdx;
                // convert this table
                TfmDlgWait.i_ShowModalWaitF(@fmDlgWait, self, OnUtfConvertInitP, OnUtfConvertContinueF, OnUtfConvertDoP);
                axUtfFields[iUtf8CurrentIdx].xDs.Close;
              end;
          end;
      end;
      // remember, that we have converted
    InternalSetIntegerP(cdUtf8, 1);
  end;

Above we use some callback functions to do the conversion:

 
  procedure TdmBase.OnUtfConvertInitP(Sender: TObject);
  // callback from wait window: start conversion
  begin
    axUtfFields[iUtf8CurrentIdx].xDs.Open;
    axUtfFields[iUtf8CurrentIdx].xDs.First;
    fmDlgWait.btnCancel.Enabled := false;
    fmDlgWait.Width := 400;
    fmDlgWait.Left := (Screen.Width - fmDlgWait.Width) div 2;
    fmDlgWait.xlbCaption.Caption := 'Converting... Step [' + IntToStr(iUtf8CurrentIdx) + '].';
    fmDlgWait.xlbStep.Caption := 'Tabelle ' + S_ComponentNameWithoutPrefixF(axUtfFields[iUtf8CurrentIdx].xDs) + '... Please wait.';
    fmDlgWait.pbr.Max := Max(100, axUtfFields[iUtf8CurrentIdx].xDs.RecordCount);
  end;

  function TdmBase.OnUtfConvertContinueF(Sender: TObject; i_Param: integer): boolean;
  // callback from wait window: detect end of table
  begin
    result := not axUtfFields[iUtf8CurrentIdx].xDs.Eof;
  end;

  procedure TdmBase.OnUtfConvertDoP(Sender: TObject);
  // callback from wait window: do the conversion for the current table record
  var
    fPost: boolean;
    sPre : AnsiString;
    sUtf : UTF8String;
    iFld : integer;
    xFld : TField;
    xDs  : TDataSet;
  begin
    xDs := axUtfFields[iUtf8CurrentIdx].xDs;
    fPost := false;
    try
      for iFld := 0 to length(axUtfFields[iUtf8CurrentIdx].asFields) - 1 do
        begin
          xFld := xDs.FieldByName(axUtfFields[iUtf8CurrentIdx].asFields[iFld]);
          // read the string as ansi string from the database
          sPre := xFld.AsString;
          // convert this string to utf8
          sUtf := AnsiToUtf8(sPre);
          // save (just if changed)
          if (sPre <> sUtf) then
            begin
              if not fPost then
                begin // on first difference: we need to post this record
                  xDs.Edit;
                  fPost := true;
                end;
              xFld.AsString := sUtf;
            end;
        end;
      if fPost then
        begin
          xDs.Post;
        end;
    except
      xDs.Cancel;
      raise;
    end;
    xDs.Next;
  end;

 

After we converted the database, we have to ensure, that we access this fields from now as utf8. In Delphi this is easy: Every Field hat a OnGetText and OnSetText event, where we can do the correct encoding/decoding.

We implement 2 general eventhandlers:

   procedure TdmBase.OnGetTextUtf8(Sender: TField; var Text: string; DisplayText: Boolean);
  // do the utf8 decoding prior using the field internal as ansi string
  begin
    inherited;
    Text := Utf8ToAnsi(Sender.AsString);
  end;

  procedure TdmBase.OnSetTextUtf8(Sender: TField; const Text: string);
  // do utf8 encoding before write to database
  begin
    inherited;
    Sender.AsString := AnsiToUtf8(Text);
  end;

Those routines does the assignments of the field eventhandlers and
call those for every field need to be handled.

 
  procedure TdmBase.Utf8FieldAssignP(xFld: TField);
  begin
    xFld.OnGetText := OnGetTextUtf8;
    xFld.OnSetText := OnSetTextUtf8;
  end;

  procedure TdmBase.Utf8InitP;
    // init all utf8 fields: assign to GetText/SetText
    procedure IntInitP(xDs: TDataSet; asFields: array of string);
    var
      iFld: integer;
      xFld: TField;
    begin
      for iFld := 0 to length(asFields) - 1 do
        begin
          xFld := xDs.FieldByName(asFields[iFld]);
          // avoid that we have a static eventhandlers for your events already
          AssertP(not assigned(xFld.OnSetText) and not assigned(xFld.OnGetText), Format('Utf8 init fail for [%s]', [xDs.Name]));
          Utf8FieldAssignP(xFld);
        end;
    end;

  var
    iIdx: integer;
  begin
    if f_InternalCheckOkF(cdUtf8) and (i_InternalGetF(cdUtf8) <> 0) then
      begin
        for iIdx := 0 to Length(axUtfFields) - 1 do
          begin
            IntInitP(axUtfFields[iIdx].xDs, axUtfFields[iIdx].asFields);
          end;
      end;
  end;

 

Download this page

If you have questions about data conversion from ansi to UTF8 or some other decode/encode problems: Do not hessitate to contact us!

Delphi FMX TLang Component data creation

Creating TLang content external with text editor or spreadsheet

We give out TLang Converter for free: See at the end of the article how to get it!

Delphi FMX.TLang Component is suitable for simple translation / localization processes within MS Windows, Mac OS or Android projects.
Unfortunately the editor for the texts is not very comfortable.
There is a Import-Function for new languages in Text format, but this mean, on every text change you have to import all files for all languages new.
Further there are some bugs and issues in the internal editor.
Also there is no way to delete languages – e.g. to import them from an external file again.
This mean, every time you change some languages you have to clear the component and load all files one by one into the component. Handling many files for different languages is further very prone to failure and not very comfortable to handle.

To enable a more comfortable editing of text resources we developed a CSV converter for the TLang Text format.
This enable
– editing the texts all in one place such as in a spreadsheet program like LibreOffice, OpenOffice or Excel.
– generating the texts out of databases or other tools.

Delphi FMX TLang Converter

The converter read the CSV File (Ansi or UTF-8) and write the binary LNG output file.

TLang.Spreadsheet

Sample of a source spreadsheet to edit the language texts.

After you have created the file, you can load this file in TLang with the function „Load file…“ within the TLang property editor (Doubleclick on the TLang component).

Important:
The TLang component have to be „initial“ befor loading files.
Either you’ve just created it or you delete the binary content of the section „ResourcesBin“ inside the FMX resource file.

object Lang1: TLang
     Lang = 'de'
     Left = 208
     Top = 32
     ResourcesBin = {
       68006900730020007400000770069006C006C00
       20006200650020007400400650064000D000A00
       ... }

Delete the whole entry „ResourcesBin“ prior import the LNG file.

After you have loaded your LNG file do not edit the content within the property editor: There seems to be another bug and some texts will be mixed up.
Just close the property editor after loading the LNG file and trust everything is ok.

Sample Project to show the TLang functions

This program show the use of the TLang component. The strings were be loaded by the converter program into TLang. There are sample spreadsheet data in ANSI and UTF16 to show the handling of external data.

SampleEnSampleDeSampleFrSampleZn

Follow this link to download a sample program for language switching.

This program include also code to do manual requests to the TLang component.

Manual Translations also can be done with the FMX.Types.Translate() function – so this is just an expamle how to work with the TLang component.

Usually the component translate everything on the form (or on its way to the form) itself. For example: When you assign a string to a TLabel, this string will be translated by the component, without any other code:

Label1.Text := 'English'

will result in

‚German‘ for that Label on the screen, if there is a according mapping in the translation resources of the component.
Non-styled components or dialogs do not support those mechanism. If you want to translate those texts, you have to do it by your own. Either you do this with the FMX Translate() or TranslageText() Function or with some code like this, using the TLang component:

Text1.Text := sTextManualF('English');

The routine to get certain text map entry out of TLang:

  function TfmMainSampleTLang.sTextManualF(sOrg: string): string;
  // translate a text, which is not in the automate translation process of TFmxObjects
  begin
   // first check, if the selected language has a mapping
    if Lang1.Resources.IndexOf(Lang1.Lang) >= 0 then
      begin
        // get the resource stringlist of the current language and get the translation
        result := Lang1.LangStr[Lang1.Lang].Values[sOrg];
        if result = '' then
          begin
          // text not found in mapping - keep original text
            result := sOrg;
          end;
      end
    else
      begin // language not found: must be the default language
        result := sOrg;
      end;
  end;

This routine of course could be placed inside a descendant of TLang individual to your project.

 

Getting the converter for free

We can give you the converter for free as freeware without cost.
Only thing we want is a link to our (this) homepage – to make this small company page a bit more famous 🙂

If you want to get the converter just send us a mail with the link and you got a free copy of the software.

Optional we offer services to do localizations or translations in our office here in Heidelberg for any Delphi VCL or FMX project.

Please contact by our main mail address in the contact page.

 

For very complex localization tasks you may want to use a external tool like
http://www.tsilang.com
http://www.sisulizer.de
http://www.regulace.org
– but for smaller projects or simple translation purposes we think, the TLang Component is not that bad than some may think after their first experience – especially if you use our converter.

Nixie-Röhren

Nachdem auf Heise News von einem Startup berichtet wurde
welches sich mit Nixie-Röhren befasst wurde mir bewusst,
dass ich unserem Elektronik-Lager auch mal wieder etwas Aufmerksamkeit
schenken könnte:
Die letzten Jahre hat doch die Softwareentwicklung in unserer Tätigkeit überwogen.
Überrascht hat mich, dass die Rodan CD27 und CD47 innerhalb der
letzten 25 Jahre nun zu wirklichen Raritäten geworden sind.

Grund genug die übergroßen Schönheiten nun wenigstens einmal
der Öffentlichkeit als Bild zu präsentieren: Oben die beiden CD27 Tubes, unten die CD47 Nixie Tube.

Rodan Nixie tube CD27

 

 

Rodan Nixie tube CD27

 

 

Rodan Nixie tube CD47

 Die passende Anwendung fehlt mir immer noch – so dass sie leider wieder in den Keller müssen.

 

800

Windows Standardverzeichnisse

Immer wieder stellt sich die Frage, welche Windowsordner über die SHGetFolderLocation / SHGetSpecialFolderLocation Funktionen zurückgegeben werden.
 Hier eine Auflistung der Ergebnisse (am Beispiel eines Benutzers „Test“):

CSIDL_DESKTOP C:\Users\test\Desktop
CSIDL_PROGRAM_FILES C:\Program Files (x86)
CSIDL_PROGRAM_FILESX86 C:\Program Files (x86)
CSIDL_PROGRAM_FILES_COMMON C:\Program Files (x86)\Common Files
CSIDL_PROGRAM_FILES_COMMONX86 C:\Program Files (x86)\Common Files
CSIDL_COMMON_APPDATA C:\ProgramData
CSIDL_COMMON_STARTMENU C:\ProgramData\Microsoft\Windows\Start Menu
CSIDL_COMMON_PROGRAMS C:\ProgramData\Microsoft\Windows\Start Menu\Programs
CSIDL_COMMON_STARTUP C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup
CSIDL_COMMON_ALTSTARTUP C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup
CSIDL_COMMON_TEMPLATES C:\ProgramData\Microsoft\Windows\Templates
CSIDL_COMMON_DESKTOPDIRECTORY C:\Users\Public\Desktop
CSIDL_COMMON_DOCUMENTS C:\Users\Public\Documents
CSIDL_COMMON_MUSIC C:\Users\Public\Music
CSIDL_COMMON_PICTURES C:\Users\Public\Pictures
CSIDL_COMMON_VIDEO C:\Users\Public\Videos
CSIDL_PROFILE C:\Users\test
CSIDL_LOCAL_APPDATA C:\Users\test\AppData\Local
CSIDL_CDBURN_AREA C:\Users\test\AppData\Local\Microsoft\Windows\Burn\Burn
CSIDL_HISTORY C:\Users\test\AppData\Local\Microsoft\Windows\History
CSIDL_INTERNET_CACHE C:\Users\test\AppData\Local\Microsoft\Windows\Temporary Internet Files
CSIDL_APPDATA C:\Users\test\AppData\Roaming
CSIDL_COOKIES C:\Users\test\AppData\Roaming\Microsoft\Windows\Cookies
CSIDL_NETHOOD C:\Users\test\AppData\Roaming\Microsoft\Windows\Network Shortcuts
CSIDL_PRINTHOOD C:\Users\test\AppData\Roaming\Microsoft\Windows\Printer Shortcuts
CSIDL_RECENT C:\Users\test\AppData\Roaming\Microsoft\Windows\Recent
CSIDL_SENDTO C:\Users\test\AppData\Roaming\Microsoft\Windows\SendTo
CSIDL_STARTMENU C:\Users\test\AppData\Roaming\Microsoft\Windows\Start Menu
CSIDL_PROGRAMS C:\Users\test\AppData\Roaming\Microsoft\Windows\Start Menu\Programs
CSIDL_ADMINTOOLS C:\Users\test\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Administrative Tools
CSIDL_STARTUP C:\Users\test\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
CSIDL_ALTSTARTUP C:\Users\test\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
CSIDL_TEMPLATES C:\Users\test\AppData\Roaming\Microsoft\Windows\Templates
CSIDL_DESKTOPDIRECTORY C:\Users\test\Desktop
CSIDL_PERSONAL C:\Users\test\Documents
CSIDL_MYDOCUMENTS C:\Users\test\Documents
CSIDL_FAVORITES C:\Users\test\Favorites
CSIDL_COMMON_FAVORITES C:\Users\test\Favorites
CSIDL_MYMUSIC C:\Users\test\Music
CSIDL_MYPICTURES C:\Users\test\Pictures
CSIDL_MYVIDEO C:\Users\test\Videos
CSIDL_WINDOWS C:\Windows
CSIDL_FONTS C:\Windows\Fonts
CSIDL_RESOURCES C:\Windows\Resources
CSIDL_SYSTEM C:\Windows\System32
CSIDL_SYSTEMX86 C:\Windows\SysWOW64

Automatische Publizierung mit Powerpoint und Excel

Für die Daimler AG haben wir eine komplexe, vollautomatische Publizierungslösung für Datenbankinhalte entwickelt.
Da die entwickelte Lösung auch für anderes Publishing sehr universell einsetzbar ist hier eine grobe Beschreibung des Publikationssystems.

Ziel des Systems war es, große Datenmengen, primär aus unserer QuiX² Datenbank, aber auch aus anderen Datenquellen zu Berichten zusammenfassen. Da sich viele Berichte nur durch einzelne Berichtsparameter unterscheiden sollen Templates verwendet werden.
Somit können Berichte mit unterschiedlichen Parametern mit nur einem Layout abgedeckt werden.
Berichte sollen kombiniert, gruppiert und geschachtelt werden können: Berichtsteile sollen somit wiederum Teile anderer Berichte sein können.

Eine besondere Vorgabe war das Verwenden von MS Office 2010 als Berichtsgenerator.
Dies scheint zunächst aufwändig und umständlich: Gibt es doch sehr viele Reportingtools und Reportgeneratoren am Markt, wie z.B. Combit List&Label, FastReport oder Crystal Report. Diese bieten bereits sehr viel Funktionalität und lassen sich direkt über die Datenbank mit Daten versorgen.
Programmierschnittstellen zu diesen kommerziellen Berichtsgeneratoren lassen eine sehr hohe Konfiguration und weitgehende Eingriffsmöglichkeiten auf das Berichtsresultat zu.
Trotzdem wurde auf Wunsch des Kunden die Lösung über Excel- und Powerpoint realisiert.
Dies hat eine Reihe von Vorteilen gegenüber den oben genannten Berichtstools:

  • Die Ergebnisdateien liegen im Rohformat vor und sind direkt weiter verarbeitbar.
  • Keine Einschränkung auf PDF Ausgabe.
  • Berichtsergebnisse können noch in der Ausgabedatei angepasst werden.
  • Keine Benutzerschulungen für externe Produkte zur Berichtgenerierung.
  • Speziell in Excel gute Kombinationsmöglichkeit von beliebig vielen Grafiken in einer Ausgabe.
  • Hohe Konfigurierbarkeit durch den Anwender.
  • Über Makros kann der Arbeitsablauf weitestgehend automatisieren lassen.
  • Über Makros sind auch für den Endkunden einfache Modifikationen der generierten Berichte möglich.

Die Makrofähigkeit der Office Produkte konnte verwendet werden, um sehr komplexe Berichte aus vielen Einzelelementen dynamisch zusammen zu stellen.

Berichtsinhalte werden aus der Datenbank im Database Publishing Prozess als Excel, Word oder Powerpoint Element generiert. Diese einzelnen Extrakte sollen dann zu PPT Dateien Dateien zusammengefasst werden (ebenso wäre eine Generierung als Word Datei möglich). Eine anschliessende Verteilung erfolgt als PDF Datei oder als Html/WEB Upload über das Intranet.

Das Zusammenfassen der Extrakte in PPT Dateien erfolgt über generische Vorlagedateien: Durch eine Steuerdatei wird ein Bericht mit seinen Varianten definiert. Die Vorlagen selbst sind universell einsetzbar und können für mehrere Endberichte oder Berichtsvarianten identisch sein.

Das Einbetten der externen Inhalte erfolgt automatisch über die im Bericht definierten Befehle.
Der Verweis auf externe, einzubindende Daten erfolgt hierbei in der PPT Datei nicht statisch sondern wird programmgesteuert vorgenommen. Die Verweise werden optional dynamisch generiert. Als Parameter für die Generierung können eine vielzahl möglicher Variablen dienen, welche über die Templatesteuerung zur Verfügung gestellt oder ermittelt werden.

Wiederholende Berichtsteile können als Gruppe zusammengefasst werden. Für die Gruppe können dann dynamische Wiederholungen auf Basis von Bedingungen oder vorhandener Quelldaten definiert werden.
Wiederholrate oder Sichtbarkeit einer Gruppe kann ebenso über die Templatesteuerung kontrolliert werden.

Die Funktionen der Templatesteuerung im Einzelnen:
– Substitution von Textinhalten durch Werte der Templatesteuerung
– Substitution von Textinhalten durch Formeln
– Ein-/Ausblenden von Templateseiten
– Gruppierung von Seitenbereichen
– Ein-/Ausblenden von Gruppierungen
– Beliebiges Wiederholen von Seiten oder Gruppen
– Verschachteln von Gruppierungen
– Dymamisches Generieren von Links zu externen Daten
– Enumerationen für Feldwerte aus der Templatesteuerung (Matrixinhalte)
– Getrennte Prozesse für Seitengenerierung und Inhaltspublizierung
– Automatisieren der Publikationsschritte

Vorteile des Systems gegenüber einer manuellen Erstellung von Berichten:
– Hohe Redundanzvermeidung bei vielen unterschiedlichen Berichten mit ähnlichen Inhalten.
– Einfachste Änderungen an Inhalt und Formaten durch die konsequente Verwendung redundanzfreier Templates.
– Hohe Prozesssicherheit durch klar definierte Abläufe.
– Kosteneinsparung durch hohen Automatisierungsgrad.

Vorteile des Systems gegenüber einem Reportgenerator wie Combit List & Label oder Crystal Reports:
– Kein Fremdsystem, mimimaler Einarbeitungsaufwand durch Verwendung der MS Office Produkte.
– Kein Programmieraufwand: Alle Berichtsdefinitionen werden direkt in Powerpoint oder Excel vorgenommen.
– Alle Rohdaten stehen als einfache Excel oder Word Dateien auch zu anderweitiger Verwendung zur Verfügung.
– Einfacher manueller Eingriff in jeder Stufe der Berichtsgenerierung.
– Preis (keine Lizenzkosten der Reportgeneratoren).

Zyklische Berichte mit unterschiedlichen Inhalten können somit auf Knopfdruck tagesaktuell generiert werden.

 

Beispiel einer Steuerdatei:

Diese einfache Steuerdatei definiert die Berichtsvarianten.

Templatesteuerung

Auf der ersten Seite werden die Berichte selbst definiert.
Auf den weiteren Seiten werden die einzelnen Gruppen definiert.
Als Besonderheit dieses Beispiels ist auf der ersten Seite eine Enumeration definiert:
Berichtsinhalte können einfach in der Matrix angewählt werden und werden dann in der gewünschten Folge ausgegeben.

Beispiel eines dynamischen Templates:

beispielseite berichtstemplate

Ein dynamisches Template wird durch die Verwendung von Steuerobjekten aufgebaut.

stage1 dynamic powerpoint template

Die roten Objekte steuern den ersten Schritt der Publizierung: Sie bestimmen z.B. die Wiederholung einer Gruppe.
Im ersten Ablaufschritt werden diese Objekte aufgelöst und ein berichtsspezifisches Template erstellt.
Die blauen Objekte definieren Links zu externen Daten und Dateien – also z.B. „Excel in Powerpoint“ Anweisungen.
Die Verweise der Datencontainer können entweder statisch schon im Grundtemplate definiert sein oder werden dynamisch aus der Templatesteuerung befüllt.

stage2 powerpoint ready for content

Nach dem ersten Schritt sind Gruppierungen und Ausblendungen umgesetzt.
Im nächsten Prozessschritt werden nun die Inhalten an die gewünschte Position publiziert.

stage3 powerpoint with external excel content

Ein einzelner, fertiger Bericht beinhaltet dann exakt die gewünschte Information.

Kontaktieren Sie uns bei Interesse an dieser Lösung, damit wir Ihnen ein kostengünstiges Angebot auf Produkt oder Projektbasis erstellen können.

 

Powerpoint Sheets per Code kopieren

Das Kopieren von Powerpoint Sheets per VBA Programmcode kann sich als problematisch erweisen:

Es sind zwar einige Beispiele hierzu im Netz vorhanden, diese sehen meist wie folgt aus:

ActivePresentation.Slides.Range(1).Select
ActiveWindow.Selection.Copy
ActiveWindow.View.Paste 

Dies funktioniert, jedoch nur, wenn in Powerpoint die linke Slides-Auswahl den Focus hat.

Wird in Powerpoint durch vorangehenden Makro-Code der Focus auf das Slide selbst gesetzt erscheinen Fehlermeldungen wie:

Run-time error ‚-2147188160 (80048240)‘: Selection (unknown member): Invalid request. Nothing appropriate is currently selected.

Run-time error ‚-2147188160 (80048240)‘: View (unknown member): Invalid request. Clipboard is empty or contains data which may not be pasted here.

Um das Problem zu lösen können die Objekte Active… durch die absoluten Objekte ersetzt werden – damit ist die Funktion des Codes sichergestellt, unabhängig davon, welches Anwendungselement innerhalb Powerpoints aktuell fokusiert ist:

 

Sub BeispielPowerpointSeitenKopieren()
  Dim taSlides() As Integer
  ReDim taSlides(0 To 1)
  taSlides(0) = 3
  taSlides(1) = 4
  ActivePresentation.Slides.Range(taSlides).Select
  ActivePresentation.Slides.Range(taSlides).Copy
  ActivePresentation.Slides.Paste
End Sub

Dieses Beispiel zeigt zudem wie gleich mehrere Slides zum kopieren markiert werden können.

Das Objekt ActivePresentation kann jetzt noch durch eine Objektvariable ersetzt werden – so dass dann auch unabhängig ist, welche Präsentation im Moment des Ausführens aktiv ist. 

Sqlite Transaktionen unter Zeos

Die Sqlite Kopplung unter der Zeos Bibliothek verhält sich im Bereich der Datenbanktransaktionen nicht ganz wie erwartet:

Es wäre zu erwarten, dass eine manuelle Transaktionssteuerung immer Vorrang vor einer impliziten Steuerung hat – dem ist jedoch nicht so.

Ist in der TZConnection Komponente das Transaktionslevel „TransactIsolationLevel“ = tiNone und die Eigenschaft „AutoCommit“ = TRUE funktioniert jedes Post reibungslos, solange keine expliziten Transaktionen verwendet werden.

Bei expliziten Transaktionen über StartTransaction, Commit/Rollback werden schlicht keine Transaktionen angewendet: Die Performance der Datenbank ist dann entsprechend schlecht.
Damit Transaktionen wirksam werden, muss das Transaktionsisolationslevel z.B. auf tiReadCommitted gesetzt werden.

Ungeschickter Weise funktionieren in diesem Modus dann jedoch die impliziten Transaktionen nicht mehr: Die Änderungen der Datenmenge ohne expliziter Transaktion werden nicht mehr auf die DB zurückgeschrieben sondern nur noch im internen Puffer geändert. Eine Fehlermeldung erfolgt nicht. Das Problem wird erst beim nächsten Connect der Datenbank ersichtlich.

Um das Problem zu lösen muss das Property „TransactIsolationLevel“ vor dem Start einer Transaktion auf   tiReadCommitted gesetzt werden und nach einem Eintragen oder Zurückrollen der Änderungen wieder auf tiNone gesetzt werden. Zumindest kann diese Betriebsart in der Zeos Bibliothek umgeschaltet werden, ohne dass ein Disconnect/Connect erforderlich wäre.

Beispiel eines kompletten funktionstüchtigen Ablaufs mit und ohne expliziter Transaktionssteuerung:

(Einfügen von Datensätzen in eine Tabelle)

  ZConnection1.Connect;
  // 1.) einzelner datensatz - implizite transaktion
  ZConnection1.TransactIsolationLevel := tiNone;
  ZQuery1.Open;
  ZQuery1.Insert;
  ...
  // datensatz wird in die db geschrieben
  ZQuery1.Post;

  // 2.) viele datensaetze - bessere performance durch explizite transaktion
  ZConnection1.TransactIsolationLevel := tiReadCommitted;
  try
    ZConnection1.StartTransaction;
    try
      for i := 0 to 100 do
        begin
          ZQuery1.Insert;
          .. .
            ZQuery1.Post;
        end;
      ZConnection1.Commit;
      // viele daten wurden per transaktion in die db geschrieben
    except
      ZConnection1.Rollback;
      raise;
    end;
  finally
    ZConnection1.TransactIsolationLevel := tiNone;
  end;

 

Sqlite: Numerische Prüfungen

Schön in Sqlite ist, dass Typkonvertierungen/Casting relativ selbstständig und automatisch abgehandelt wird. Es gibt jedoch Situationen, in denen es notwendig ist, festzustellen, ob z.B. die Werte in einer Textspalte numerisch sind. Eine „IsNumber“ oder „TO_NUMBER“ Funktion zum Überprüfen auf einen Zahlenwert fehlt. Wenn es um Zahlen in Textfragmenten geht kann diese Funktion „Prüfen ob Zahl“ einfach nachgebildet werden: lower(spalte) = upper(spalte). Eine weitere Prüfung kann mit abs(x) oder round(x,y) durchgeführt werden.

Die Resultate anhand von einigen Beispielen. Zugrundegelegt wird die Tabelle „table1“ mit einem Feld „f1“  (char[99]).

Der Tabelleninhalt (select * from table1):

NoNumber
1
55
66,123
66.123
01.12.2011
BitBumper123
123BitBumper
0
?

Ein Select liefert die möglichen Funktionen zur Nullbestimmung in dieser Übersicht:

select  round(f1),  round(f1,99), abs(f1),  abs(f1)<>0,   f1=0, upper(f1)=lower(f1), * from table1

RecNo

round(f1)

round(f1,99)

abs(f1)

abs(f1)<>0

f1=0

upper(f1)=lower(f1)

f1

1

0

0

0

0

0

0

NoNumber

2

1

1

1

1

0

1

1

3

55

55

55

1

0

1

55

4

66

66

66

1

0

1

66,12

5

66

66.123

66.123

1

0

1

66.123

6

1

01.12.11

01.12.11

1

0

1

01.12.11

7

0

0

0

0

0

0

BitBumper123

8

123

123

123

1

0

0

123BitBumper

9

0

0

0

0

1

1

0

10

0

0

0

0

0

1

?

Eine nicht perfekte aber immerhin für viele Fälle hinreichende Variante liefert also die Kombination von upper/lower mit abs:

select f1 from table1 where (abs(f1)<>0 or f1=0) and (upper(f1)=lower(f1))

 

f1

1

55

66,12

66123

01.12.11

0

Der Datumseintrag wird als Zahl erkannt, was nicht in jedem Fall gewünscht sein dürfte.