Dienstag, 30. Oktober 2007

Visual Studio Macro zum finden des aktuellen Dokuments im Solution-Explorer (Projektmappe)

Die Option "Track Active Item in Solution Explorer" (unter Tools/Options/Projekts and Solutions/) habe ich generell deaktiviert, da es mich nervös macht wenn beim wechseln zwischen geöffneten Dateien die Solution-Explorer Ansicht auf und ab springt.

Bei umfangreichen Solutions mit vielen Projekten und Ordnern kann es so gelegentlich schwierig werden herauszufinden in welchem Ordner die gerade bearbeitete Datei abgelegt ist.

Folgendes Visual Studio Macro (getestet unter VS 2005 und VS 2008 Beta2) öffnet im Solution Explorer den Ordner unter dem die angezeigte Datei abgelegt ist.

   1 Imports System
2 Imports EnvDTE
3 Imports EnvDTE80
4 Imports System.Diagnostics
5
6 Public Module FindFileInSolutionExplorer
7 Sub FindFileInSolutionExplorer()
8
9 Dim myDTE As EnvDTE80.DTE2 = DTE
10 'myDTE = DTE
11
12 myDTE.Solution.FindProjectItem(myDTE.ActiveDocument.FullName).ExpandView()
13
14 End Sub
15
16 End Module

Einfügen kann man das Macro mit der Macros IDE (Tools/Macros/Macros IDE oder Alt-F11). Im Project Explorer "Add Class" wählen und den Code hineinkopieren.

Noch schnell ein Symbol in der Taskleiste dafür erstellt und schon sollte es funktionieren.

Leider wird nur der Ordner geöffnet in dem die Datei liegt, die Datei aber nicht ausgewählt.
Nachdem man den richtigen Ordner gefunden hat sollte es aber nicht mehr schwierig sein die Datei zu finden.
Vielleicht komme ich noch dahinter wie sich dieses Manko ausmerzen lässt ...

Samstag, 20. Oktober 2007

Zweidimensionales Array in einer C++ COM-Komponente erzeugen und an VB/VBA zurückgeben

Ich habe den Code aus einer konkreten Anwendung herausgelöst und die Bezeichner verallgemeintert, das Ergebnis aber nicht getestet.
Womöglich habe ich den einen oder anderen Tippfehler oder ähnliches hinzugefügt ...

Ein Beispiel für SafeArrayGetElement() fehlt noch.
Bei Gelegenheit werde ich das nachzuholen.

   1 // MyCOM.idl
2 interface ICo4COM : IUnknown {
3 HRESULT GetArray([out,retval] SAFEARRAY(short)* arr);
4 }
5
6 // MyCOM.h
7 interface IMyCOM : public IUnknown {
8 virtual HRESULT __stdcall GetArray(SAFEARRAY(short)** arr) = 0;
9 }
10
11 // MyCom.cpp
12 class MyCOMImpl : public ICo4COM {
13 private;
14 SAFEARRAY* arr;
15 SAFEARRAYBOUND rgsabound[2];
16
17 ~Co4COMImpl() {
18 gObjCnt--;
19
20 if(arr != NULL)
21 {
22 SafeArrayDestroy(arr);
23 arr = NULL;
24 }
25 }
26
27 HRESULT __stdcall GetArray(SAFEARRAY** arr) {
28 *arr = this->arr;
29 return S_OK;
30 }
31
32 void AddElement(long x, long y, short val) {
33 long index[2];
34 index[0] = x;
35 index[1] = y;
36
37 short data = val;
38 SafeArrayPutElement(arr, index, &data);
39 }
40
41 long ArrayXCount() {
42 return long(rgsabound[0].cElements);
43 }
44
45 long ArrayYCount() {
46 return long(rgsabound[1].cElements);
47 }
48
49 bool InitArray(long sizeX, long sizeY) {
50 short data = 0;
51 long index[2];
52
53 rgsabound[0].lLbound = 0;
54 rgsabound[0].cElements = sizeX;
55 rgsabound[1].lLbound = 0;
56 rgsabound[1].cElements = sizeY;
57
58 if(arr != NULL)
59 SafeArrayDestroy(arr);
60
61 arr = SafeArrayCreate(VT_I2, 2, rgsabound); // array of short
62
63 if(arr == NULL) { //SafeArray create failed
64 return false;
65 }
66
67 // init with 0
68 for(long i = 0; i <>cols; i++) {
69 for(long j = 0; j <>rows; j++) {
70 index[0] = i;
71 index[1] = j;
72 data = 0;
73
74 SafeArrayPutElement(arr, index, &data);
75 }
76 }
77
78 return true;
79 }
80 }

Verstrichene Zeit in C++ unter Windows messen ...

hat mich einige Zeit gekostet eine Lösung zu finden.
Fündig wurde ich unter Get time in milliseconds in Windows

Nachfolgend ein Beispiel wie ich es konkret eingesetzt habe.

   1 ULONGLONG endCalculationAt;
2
3 ULONGLONG GetSystemTimeInMS() {
4 SYSTEMTIME systemTime;
5 GetSystemTime(&systemTime);
6
7 FILETIME fileTime;
8 SystemTimeToFileTime(&systemTime, &fileTime);
9
10 ULARGE_INTEGER uli;
11 uli.LowPart = fileTime.dwLowDateTime; // could use memcpy here!
12 uli.HighPart = fileTime.dwHighDateTime;
13
14 ULONGLONG systemTimeIn_ms(uli.QuadPart/10000);
15 return systemTimeIn_ms;
16 }
17
18 void InitCalculationTime() {
19 endCalculationAt = GetSystemTimeInMS() + (ULONGLONG)(10 * 1000); // ab jetzt 10 Sekunden Zeit
20 }
21
22
23 bool CheckCalculationTimeElapsed() {
24 if(GetSystemTimeInMS() >= endCalculationAt)
25 return true;
26 else
27 return false;
28 }

Buttons dynamisch zur Laufzeit hinzufügen ist in Excel leicht realisiert, aber ...

wie bringt man diese Buttons dazu dass ein Klick darauf etwas sinnvolles macht?

Eine Variante, die ich im Netz gefunden habe, ist entsprechende Ereignisprozeduren dynamisch zu erzeugen.
Eine weitere gefundene Variante die mir besser gefällt habe ich nachfolgend zusammengefasst.

[hinzugefügt am 2008-11-18 12:15]
Ein vollständiges lauffähiges Beispiel steht unter diesem Link zum Download bereit.
Die Codeteile sind im VBA-Editor unter "Diese Arbeitsmappe", "DynamicButtonSheet (Tabelle1)" und im Klassenmodul "ButtonObject" zu finden.

Nachfolgend die wichtigsten Codeteile mit einigen Anmerkungen:
[hinzugefügt bis hier]

Folgendes Klassen-Modul wird benötigt.

   1 Option Explicit
2
3 Public WithEvents ButtonObject As MSForms.CommandButton
4
5 Public btnName As String
6 Public btnId As Integer
7
8 Private Sub ButtonObject_Click()
9 Module1.MoveButton_Click btnId
10 End Sub

Der globalen Variable ButtonObject wird später der neu erstellt Button zugewiesen.
btnName und btnId sind Beispiele für zusätzliche Properties zur Identifikation welcher konkrete Butten gedrückt wurde.

ButtonObject_Click() ist die Ereignis-Prozedur die im Fall eines Button-Click aufgerufen wird.
Module1.MoveButton_Click btnId ist eine zentrale Prozedur in der die Button-Click-Ereignisse verarbeitet werden, an die der Aufruf weitergeleitet wird.

Es folgt ein Beispielcode wie die Buttons erstellt werden.

   1 Private Buttons As New Collection
2
3 Dim btnWidth as Integer
4 Dim btnHeight as Integer
5
6 btnWidth = 50
7 btnHeight = 25
8
9 For i = 0 To 10
10 Set btn = ws.OLEObjects.Add(ClassType:="Forms.CommandButton.1", Link:=False, DisplayAsIcon:=False, _
11 Left:= i * btnWidth, _
12 Top:= 200, Width:=btnWidth, Height:=btnHeight)
13 btn.Name = RT_PREFIX & "MoveButton" & i
14 btn.Object.Caption = "Button " & i
15 btn.Object.TakeFocusOnClick = False
16 Next i

Nach dem Erstellen der Buttons kommt ein kleiner Trick der Excel eine kurze Verschnaufpause verschafft damit es nicht außer Tritt kommt.

   1 Application.OnTime Time + TimeSerial(0, 0, 1), "Module1.prcAssign"

Damit wird die Prozedur prcAssign aus dem Module1 zeitversetzt aufgerufen.
prcAssign sieht folgendermaßen aus:

   1 Public Sub prcAssign()
2 Dim ws As Worksheet
3 Dim btn As OLEObject
4 Dim button As ButtonObject
5 Dim i As Integer
6
7 Set ws = ActiveWorkbook.Worksheets("Tabelle1")
8 i = 0
9
10 For Each btn In ws.OLEObjects
11 If Left(btn.Name, Len(RT_PREFIX)) = RT_PREFIX Then
12 Set button = New CButton
13 Set button.ButtonObject = btn.Object
14 button.Id = i
15 button.Name = RT_PREFIX & "Button" & i
16 Buttons.Add button
17
18 i = i + 1
19 End If
20 Next
21
22 End Sub

Beim Zuordnen eines Buttons in der beschriebenen Art an die ButtonObject Variable einer CButton Instanz wird automatisch die Ereignisprozedur ButtonObject_Click zugeordnet.

Freitag, 19. Oktober 2007

Globale Variablen in Excel verlieren ihren Wert

Ein bekanntes Problem scheint zu sein, dass Excel (ich hatte das Problem mit Excel 2003) während der Laufzeit aus verschiedenen Anlässen die Werte globaler Variablen "vergisst".

Lt. Informationen aus dem Web passiert das wenn

  • der Code neu compiliert wird
  • ein Laufzeitfehler auftritt
Wenn Änderungen am Code durchgeführt werden ist es nachvollziehbar, dass neu compiliert werden muss.
Die Werte gehen aber auch verloren wenn keine Änderungen gemacht wurden und keine Laufzeitfehler aufgetreten sind (ist mit "Unterbrechen bei Fehlern" auf "Bei jedem Fehler" gesetzt leicht überprüfbar).

In meinem Fall dürfte die Ursache daran gelegen sein, dass ich zur Laufzeit per Programmcode Buttons hinzugefügt und entfernt habe (siehe Beitrag Buttons dynamisch zur Laufzeit hinzufügen ist in Excel leicht realisiert, aber ...).
Dadurch entsteht die Notwendigkeit den Programmcode neu zu kompilieren und das führt zum hier beschriebenen Phänomen.

Nach langer erfolgloser Suche im Web bin ich auf folgende Lösung gekommen, die in meiner Situation das Problem gelöst hat

Ich habe eine neue leere Arbeitsmappe erstellt und in ein Codemodul folgenden Code eingefügt

   1 Option Explicit
2
3 Public Co4COMObj As SWK5_P1.Co4COM
4
5 Public Function Co4COM() As SWK5_P1.Co4COM
6 If Co4COMObj Is Nothing Then
7 Set Co4COMObj = New SWK5_P1.Co4COM
8 End If
9
10 Set Co4COM = Co4COMObj
11 End Function

und die Arbeitsmappe als "Microsoft Office Excel-Add-In (*.xla)" gespeichert.

Bei diesem Beispiel geht es darum, dass eine Instanz einer COM-Komponente (SWK5_P1.Co4COM) erstellt wird und diese über einen längeren Zeitraum verfügbar bleiben soll.

Nach dem Prinzip des Singleton-Patterns wird geprüft ob die Variable Co4COMObj schon zugwiesen wurde.
Wenn ja wird sofort eine Referenz zurückgeliefert andernfalls wird zuvor mit New eine neue Instanz zugewiesen.

In der Arbeitsmappe, in der die globale Variable verwendet wird, wird diese *.xla (in diesem Fall Co4COM.xla) über das "Menü Extras/Add-Ins..." eingebunden.

Eine Prozedur kapselt den Zugriff darauf:

   1 Public Function Co4COM() As SWK5_P1.Co4COM
2 ' to trick the damned Excel loosing the value of global variables occassionally,
3 ' the public variable holding the COM component was moved to an AddIn
4 Set Co4COM = Application.Run("Co4COM.xla!Co4COM")
5 End Function

Hieraus wird ersichtlich, dass der Zugriff auf die Prozedur im Add-In, die eine Referenz auf die gewünschte Variable zurückliefert, etwas umständlicher ist als gewohnt.

Es können meines Wissens auch keine Variablen im Add-In direkt angesprochen werden sondern es müssen entsprechende Getter- und Setter-Prozeduren bzw Functions bereitgestellt werden.