Posts Tagged ‘Delphi’

Fehler in TShellTreeView, TShellComboBox und TShellListView

Samstag, April 30th, 2011

Meinen letzten Artikel, den ich einem Delphi-Problem gewidmet habe, liegt schon einige Monate zurück. Da ich beim Testen auf ein größeres Problem gestoßen bin und auch recht lange gebraucht habe um das Problem zu lösen, bietet es sich an, die Lösung des Problemes hier vorzustellen.

In Delphi 2005 gibt es 2 Sets an Komponenten, die es ermöglichen, einen Dateiauswahl-Dialog recht schnell zusammenzubauen. TDriveCombobox, TDirectoryListBox und TFileListbox sind noch aus den guten alten Windows 3.1 Zeiten vorhanden. Von der Optik her würde ich heute diese Komponenten nicht mehr einsetzen. Als zweites Set haben TShellTreeView, TShellComboBox und TShellListView in der Komponentenliste Platz gefunden. Allerdings werden sie etwas stiefmütterlich behandelt und befinden sich in meiner Delphi 2005 Version nur unter der Komponentenkategorie „Beispiele“.

Im BVASystem habe ich den TShelltreeview und die TShellComboBox genutzt, um den Dialog zur Auswahl des Dateinamens zur Speicherung eines Fotoindexes zu realisieren. Beim Testen ist mir dann aufgefallen, das die Komponenten unter Windows XP für Verzeichnisse, die auf einer CD liegen, teilweise falsche Pfadangaben zurückgeben.  Es werden nur dann richtige Pfadangaben geliefert, wenn das Verzeichnis kein Unterverzeichnis ist. „D:\Bilder\“ klappt also, während für „D:\Bilder\2011-04-29\“  nur „D:\Bilder\“ zurückgegeben wird.

Ok, für die Auswahl eines Speicherortes kann man wohl getrost auf das CD-Rom Laufwerk verzichten. Aber da ich vor habe, die Komponenten auch anderweitig zu verwenden, hat mich das Problem nicht mehr losgelassen.

Die Ursache des Problems konnte ich in der Klasse TShellFolder und dort genau in der Funktion „PathName“ finden. Zur Korrektur des Fehlers muss die Funktion, die sich in der „ShellCtrls.pas“ befindet, folgendermaßen geändert werden:

function TShellFolder.PathName: string;
begin
 result := GetDisplayName(ParentShellFolder, FPIDL, SHGDN_FORPARSING);
 //Result := GetDisplayName(DesktopShellFolder, FFullPIDL,  SHGDN_FORPARSING );
end

Wenn man dann die „ShellCtrls.pas“ schon offen hat, bietet es sich auch an, gleich ein paar Speicherlecks, die die Komponenten haben, zu schließen. Wie die Speicherlecks beseitigt werden, könnt ihr in der QualityCentral von Embarcadero nachlesen. Dort hat Brad Prendergast seine Lösung detailiert in den Kommentaren beschrieben.

Umstellung auf TActions

Samstag, Dezember 4th, 2010

Die letzte Nacht habe ich damit verbracht, die Buttons der Funktionsleiste unter dem Vorschaubild umzustellen. Als ich vor einem Jahr begonnen habe, das neue BVASystem zu entwickeln, war die Funktionsleiste so ziemlich das erste was ich implementierte. Ich entschied mich dafür, die Toolbar in einer eigenen Komponente, der TImgViewToolbar, zu kapseln. Für die einzelnen Funktionen der Knöpfe erstellte ich Ereignisse, die ich in der Hauptanwendung implementierte. Die Steuerung über den Status der Buttons übernahm die Komponente, war nach außen hin also nicht sichtbar.

Anfang der Woche stolperte ich, dann aber beim Neugestalten des Hauptmenüs auf ein Problem. Auch im Hauptmenü müssen die einzelnen Funktionen wie in der Funktionsleiste aktiviert bzw. deaktiviert werden. Einfachste Lösung wäre sicher gewesen, die Funktion aus der TImgViewToolbar zu kopieren und ebenfalls für das Hauptmenü zu verwenden. Da ich aber Wert darauf lege, sauberen Programmcode zu schreiben, war diese Lösung für mich sofort unten durch.

Meine Wahl fiel schlussendlich dann darauf, die Funktionen über den TActions-Konstrukt zu implementieren.  In der Hauptanwendung habe ich einen TActionsmanager hinzugefügt. Dort kann man recht einfach, über einen kleinen Dialog, Aktionen definieren. Diese Aktionen können dann mit einer Reihe von Standardkomponenten verknüpft werden. Zum Beispiel können ganz leicht Menüs oder Toolbars mit den Aktionen verbunden werden. Die Funktion, die ausgeführt werden soll, wenn die Aktion aufgerufen wird, packt man einfach in das OnExecute()-Event:

procedure TfrmMain.AcImgVollbildExecute(Sender: TObject);
begin
 if g_AktQuery <> nil then begin
  if g_AktQuery.DataList <> nil then begin
   if g_AktQuery.DataList.Count > 0 then begin
    frmVollBild.Show;
   end;
  end;
 end;
end;

Der Vorteil des ganzen ist nun, wenn man eine Funktion deaktieren will, weil sie gerade nicht sinnvoll ausgeführt werden kann, so braucht man diese nur auf inaktiv zu schalten. Alle Dialogelemente, die mit der Aktion verknüpft wurden, sind dann ebenfalls deaktiviert.  Weitere Vorteile sind, das auch Hints, Bilder und Beschriftungen nur einmal zentral bei den Aktionen definiert werden müssen. Nachteil ist für mich jetzt nur, das ich die gute alte TImgViewToolbar auf TActions umstellen musste, was natürlich Arbeit bedeutete.

Diese habe ich gestern abend aber doch recht schnell erledigen können. Aus den alten Events der Komponente sind nun Eigenschaften vom Typ TAction geworden. Intern, in der Komponente, werden die Aktionen nur noch den einzelnen Knöpfen zugeordnet. Und das schöne ist, die Komponente kann sich weiterhin darum kümmern, das die Buttons aktiviert bzw. deaktiviert werden. Sie bekommt aber gar nicht mit, das dabei das Hauptmenü gleich mit aktiviert bzw. deaktiviert wird.

Kein Datenträger

Mittwoch, Oktober 20th, 2010

Schon in der Version 1.x des BVASystems ärgerte ich mich über einen Fehler, den ich nicht wirklich nachvollziehen bzw. beheben konnte. Gestern trat dieser dann auch bei der neuen Software auf.

Beim Einlesen des Verzeichnisbaumes wird überprüft, ob sich auf den Laufwerken des Computers Unterverzeichnisse befinden. Auch auf Wechseldatenträgern muss der Versuch gestartet werden, die Unterverzeichnisse einzulesen, da es keine andere Möglichkeit gibt, herrauszufinden ob sich ein Datenträger im Laufwerk befindet. Die Funktion ist seit der ersten Version des BVASystems 2.0 unverändert in der Software enthalten. Gestern zeigte sich ein Fehlerdialog mit der Aufschrift: „Es befindet sich kein Datenträger im Laufwerk. Legen Sie einen Datenträger in Laufwerk … ein.“

Diese Fehlermeldung erschien im BVASystem, wenn sich in einem Laufwerk kein Datenträger befand.

Fehlerdialog: Kein Datenträger

Auf meinem Testrechner machte sich der Fehler durch einen anderen Fehlerdialog bemerkbar. Dort kam es zu einer „Exception Processing Message c0000013“. Ohne die Überschrift hätte ich nicht verstanden, was die Fehlermeldung mir sagen will.

Auf dem Testrechner sah die Fehlermeldung dagegen so aus.

Exception: Kein Datenträger

Ich stellte fest, das die Fehlermeldung aufgetreten war, nachdem ich am Entwicklungsrechner den Kartenleser genutzt hatte, um die Bilder meine Digitalkamera auf den Rechner zu spielen. Nach einem Neustart des Rechners und einer anschließenden Neuübersetzung der Bilddatenbank war der Fehler wieder weg. Da ich meinen Kartenleser gerne weiterbenutzen will, ohne ständig den Rechner neu zu starten, suchte ich weiter.

Durch die weitere Recherche kam ich dann auf die Windowsfunktion „SetErrorMode“. Mit ihr kann eingestellt werden, wie das Betriebssystem auf bestimmte Fehler reagieren soll. Meine Vermutung ist, das der Treiber meines Kartenlesers diesen „ErrorMode“ verstellt hat und daher die Fehlermeldungen angezeigt wurden.

Unterbinden lassen sich die Fehlermeldungen nun ganz einfach dadurch, das der „ErrorMode“ auf SEM_FAILCRITICALERRORS setzt. Die Problemlösung sieht also wie folgt aus:

var OldErrorMode: Integer;
 begin
  OldErrorMode := SetErrorMode(SEM_FAILCRITICALERRORS);
  try
   ReadDirectorys();
  finally
  SetErrorMode(OldErrorMode);
  end;
 end;

Ein globales OnClick() in Delphi

Donnerstag, September 2nd, 2010

Gestern abend konnte ich eines von Murpyhs Gesetzen verifizieren, es lautet:  „In jedem kleinen Problem, steckt ein Großes, das gerne heraus möchte.“ Eigentlich wollte ich nur eine kleine Verbesserung an der Slideshow-Funktionalität vornehmen, aber dann entdeckte ich das große Problem, welches mich den ganzen Abend beschäftigte.

Folgendes Verhalten wollte ich bei der Slideshow umsetzen:

  • die Slideshow soll über den Button in der Toolleiste aktivierbar sein
  • ein zweiter Klick auf den Button soll sie wieder deaktivieren
  • und ein Klick auf irgendein anderes Steuerelement soll die Slideshow ebenfalls deaktivieren

Die ersten beiden Punkte sind einfach umzusetzen. Das Problem liegt in dem dritten Punkt. Im alten BVASystem hatte ich die Funktionalität dadurch erreicht, indem ich bei jedem OnClick() Event ein:

procedure TfrmMain.Toolbutton1Click(Sender: TObject);
begin
 if (Slideshow.Aktiv = true) then Slideshow.Aktiv := false;
 ...
end;

eingebaut habe. Das wollte ich dieses mal aber um jeden Preis vermeiden, da die Slideshow sich im neuen Programm im Hauptdialog befindet. Dort wären weit mehr Funktionen zu erweitern, als es im alten Programm in der Vollbildansicht der Fall war.

Gelöst habe ich das Problem schlussendlich mit einem globalen OnClick Ereignis.  In der Komponetensammlung von Delphi befindet sich eine Klasse TApplicationEvents, die einen Eventhandler für OnMessage() besitzt.  Dieses Event wird vor jeder Windowsbotschaft, die die Anwendung empfängt, geworfen. Damit ist es relativ leicht möglich, hier die Botschaft „WM_LBUTTONUP“ abzufangen, die Slideshow gegebenenfalls zu deaktivieren und anschließend die Botschaft an das dementsprechende Steuerelement weiterzuleiten.

Aber Vorsicht Falle. Für den Button, der die Slideshow eh deaktivieren soll, darf entweder die Slideshow nicht im OnMessage() gestoppt werden, oder aber das Event darf den Button nie erreichen. Ich entschied mich für die zweitere Variante:

procedure TfrmMain.ApplicationEventsMessage(var Msg: tagMSG; var Handled: Boolean);
var p:TPoint;
    wc:TWinControl;
    ctrl:TControl;
begin
 case Msg.message of
  WM_LBUTTONUP:begin
   //Aktuelle Mausposition bestimmen
   P.X := Mouse.CursorPos.X;
   P.Y := Mouse.CursorPos.Y;
   //TWinControl bestimmen welches sich an der Mausposition befindet
   wc := FindVCLWindow(P);
   if Assigned(wc) then begin
    //TControl bestimmen welches sich an der Mausposition befinden
    ctrl := wc.ControlAtPos(wc.ScreenToClient((P)), false, false);
    if Assigned(ctrl) then begin
     if ctrl.Name = 'btnSlideshow' then Handled := true;
    end;
   end;
   If SlideShow.Aktiv then SlideShow.Aktiv := false;
  end;
 end;
end;

Ich bestimme also zuerst die Position der Maus auf dem Desktop. Anschließend kann ich mit „FindVCLWindow“ das TWinControl bestimmen, welches sich an der MausPosition befindet. Da die Buttons aber nicht fensterorientierte Steuerelemente sind, werden sie von der Funktion nicht entdeckt. Daher ist als zweiter Schritt ControlAtPos() notwendig. Anschließend muss nur noch überprüft werden, ob das Control der Slideshowbutton ist. Ist er es, wird das Event durch „Handled := true“ aus der Windowsbotschaftenliste gelöscht.

Am Ende muss dann nur noch die Slideshow deaktiviert werden, um das von mir gewünschte Verhalten umzusetzen.