Laden...

[gelöst] SendMessage an ListView in anderem Prozess

Erstellt von OXO vor 3 Jahren Letzter Beitrag vor 3 Jahren 1.643 Views
O
OXO Themenstarter:in
86 Beiträge seit 2020
vor 3 Jahren
[gelöst] SendMessage an ListView in anderem Prozess

Hallo zusammen,

hoffe, mir kann hier jemand weiterhelfen, denn langsam gehen mir die Ideen aus und ich weiß leider nicht, was ich falsch mache.

Hier zu meinem Problem:

Ich möchte einen Text in einem ListView einer anderen Anwendung (anderer Prozess), konkret in "Windows-Fax- und -Scan", umbenennen. Leider scheitere ich am SendMessage-Aufruf und daraufhin wird "Windows-Fax- und -Scan" geschlossen.

Den Handle des ListView habe ich ermittet und deckt sich mit dem Handle, den Spy++ zeigt. Auch ein SendMessage mit LVM_GETITEMCOUNT liefert die richtige Anzahl Elemente. Ein weiterer Test, eine Zeile aus dem ListView per SendMessage zu löschen, hat auch funktioniert. Gehe davon aus, dass der Handle schon mal passt.

Leider scheitere ich mit dem SendMessage-Aufruf, um den Text für den ersten SubItem des ersten Eintrags abzuändern.

Meine Code-Fragmente schauen aktuell so aus:


[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern IntPtr FindWindowEx(IntPtr parentHandle, IntPtr childAfter, string lclassName, string windowTitle);

[DllImport("user32.dll", SetLastError = true)]
private static extern int SendMessage(IntPtr hWnd, uint Msg, int wParam, int lParam);

[DllImport("user32.dll", SetLastError = true)]
private static extern int SendMessage(IntPtr hWnd, uint Msg, int wParam, ref LV_ITEM item_info);

private struct LV_ITEM
{
    public UInt32 uiMask;
    public Int32 iItem;
    public Int32 iSubItem;
    public UInt32 uiState;
    public UInt32 uiStateMask;
    public string pszText;
    public Int32 cchTextMax;
    public Int32 iImage;
    public IntPtr lParam;
};

public const Int32 LVM_FIRST = 0x1000;
public const Int32 LVM_SETITEM = LVM_FIRST + 6;
public const Int32 LVIF_TEXT = 0x1;

private IntPtr FindMDIWindowByIndex(IntPtr hWndParent, int index)
{
    if (index == 0)
        return hWndParent;
    else
    {
        int ct = 0;
        IntPtr result = IntPtr.Zero;
        do
        {
            result = FindWindowEx(hWndParent, result, "AfxMDIFrame42u", null);
            if (result != IntPtr.Zero)
                ++ct;
        }
        while (ct < index && result != IntPtr.Zero);
        return result;
    }
}


// Get Handle of ListView
string windowTitle = "Windows-Fax und -Scan";

IntPtr ptrWnd = FindWindow(null, windowTitle);
IntPtr ptrWndMDIWindow = FindWindowEx(ptrWnd, IntPtr.Zero, "AfxMDIFrame42u", null);
IntPtr ptrWndNestedMDIWindow2 = FindMDIWindowByIndex(ptrWndMDIWindow, 2);
IntPtr ptrWndListView = FindWindowEx(ptrWndNestedMDIWindow2, IntPtr.Zero, "SysListView32", null);

LV_ITEM lvi = new LV_ITEM();
lvi.iItem = 0;             // Row.
lvi.iSubItem = 0;          // Column.
lvi.uiMask = LVIF_TEXT;    // Want to set the Text.
lvi.pszText = "Item 0";    // Text to display
             
// Send the LVM_SETITEM message.
int result = SendMessage(ptrWndListView, LVM_SETITEM, 0, ref lvi);

Meine Vermutung aktuell ist, dass das etwas mit den verschiedenen Speicherbereichen meiner Anwendung und dem fremden Prozess in dem "Windows-Fax- und -Scan" läuft zu tun hat.

Wie bekomme ich das denn realisiert, dass der Text in diesem ListView abgeändert wird? Auch ist mir unklar, wie ich ganz allgemein Strings von A nach B transportieren muss in diesem Fall?

Bin für jeden Hinweis dankbar!

4.931 Beiträge seit 2008
vor 3 Jahren

Du scheinst eine alte Struktur LV_ITEM zu benutzen - laut LVM_SETITEM message wird die Struktur LVITEMA (bzw. für Unicode/Widestrings LVITEMW benutzt) - beachte die zusätzlichen Member.

O
OXO Themenstarter:in
86 Beiträge seit 2020
vor 3 Jahren

Du scheinst eine alte Struktur LV_ITEM zu benutzen - laut
>
wird die Struktur
>
(bzw. für Unicode/Widestrings
>
benutzt) - beachte die zusätzlichen Member.

Danke schon einmal für Deine Hilfe!

Ich muss dazu vielleicht noch eine dumme Frage stellen: woher weiß ich bei den Funktionen/Strukturen/Strings bzw. beim Marshalling, ob ich das in der A- oder W-Variante für (Unicode/Widestrings) verwenden muss?

Habe noch einen interessanten Artikel zu SendMessage und genau dieser Problematik gefunden. Es scheint tatsächlich so zu sein, wie ich vermutet habe. Meine Strings und die Pointer zeigen nach der Deklaration in meiner Anwendung eigentlich auf meinen lokalen Speicher im Adressbereich meiner Anwendung. Diese müssen aber auf Speicher im Adressbereich der Ziel-Anwendung/Prozess zeigen. Daher vermute ich auch, dass das bei mir immer einfach so abgestürzt ist:

https://www.andreas-reiff.de/2011/07/win32-sendmessage-die-einen-string-erwartet-von-c-an-einen-anderen-prozess-schicken/

Habe nun die Methoden aus diesem Artikel verwendet und zumindest einen Teilerfolg damit. Immerhin stürzt es nicht mehr ab und in der richtigen Zeile/Spalte wird was angezeigt. Naja, angezeigt ist nicht ganz richtig, denn leider kommt nur ein leerer Text an und ich rätsle gerade warum? Dachte, das liegt an der fehlenden Text-Länge oder an den Mask-Parametern. Setze ich aber eine Text-Länge, schmiert das Ganze leider auch wieder ab. Mit verschiedenen Mask-Parametern hab ich auch schon experimentiert. Irgendwie scheint es mir noch nicht zu gelingen:


struct tagLVITEMA
{
	public uint mask;
	public int iItem;
	public int iSubItem;
	public uint state;
	public uint stateMask;
	public string pszText;
	public int cchTextMax;
	public int iImage;
	public IntPtr lParam;
	public int iIndent;
	public int iGroupId;
	public uint cColumns;
	public IntPtr puColumns;
	public IntPtr piColFmt;
	public int iGroup;
}

struct tagLVITEMW
{
	public uint mask;
	public int iItem;
	public int iSubItem;
	public uint state;
	public uint stateMask;
	public string pszText;
	public int cchTextMax;
	public int iImage;
	public IntPtr lParam;
	public int iIndent;
	public int iGroupId;
	public uint cColumns;
	public IntPtr puColumns;
	public IntPtr piColFmt;
	public int iGroup;
}

var item = new tagLVITEMW
{
	mask = LVIF_TEXT,
	//cchTextMax = (int)MAX_LVMSTRING,
	pszText = "Hallo",
	iItem = 1,
	iSubItem = 1
};

var result = SendMessageComplexGeneric<tagLVITEMW>((int)hWnd, LVM_SETITEM, 0, ref item, SendMessageDirection._out);

Neben diesem Effekt mit dem leeren String ist mir noch etwas aufgefallen. Wenn ich in "Windows Fax- und -Scan" im TreeView daneben auf einen Ordner klicke und das ListView mit anderen Daten gefüllt wird, und ich danach wieder zurück wechsle, so werden wieder die alten Daten im ListView angezeigt und mein "leerer Wert" in der Zelle ist wieder überbügelt mit dem alten Wert.

Die Frage ist, schaffe ich es durch das LVM_SETITEM überhaupt, die eigentlichen Daten des ListView abzuändern? Auf der Festplatte heißt die Datei bereits anders, wird aber mit dieser Aktion im ListView immer noch mit dem alten Wert angezeigt. Scheint so, als ob das ListView hier etwas cached und ich müsste diesen Wert ja irgendwie abändern.

309 Beiträge seit 2020
vor 3 Jahren

Neben diesem Effekt mit dem leeren String ist mir noch etwas aufgefallen. Wenn ich in "Windows Fax- und -Scan" im TreeView daneben auf einen Ordner klicke und das ListView mit anderen Daten gefüllt wird, und ich danach wieder zurück wechsle, so werden wieder die alten Daten im ListView angezeigt und mein "leerer Wert" in der Zelle ist wieder überbügelt mit dem alten Wert.

Die Frage ist, schaffe ich es durch das LVM_SETITEM überhaupt, die eigentlichen Daten des ListView abzuändern? Auf der Festplatte heißt die Datei bereits anders, wird aber mit dieser Aktion im ListView immer noch mit dem alten Wert angezeigt. Scheint so, als ob das ListView hier etwas cached und ich müsste diesen Wert ja irgendwie abändern.

Höchst wahrscheinlich nicht, das ist ja nur die Anzeige der Daten, glaube nicht dass das über das ListView läuft. Würde vermutlich auch so nicht funktionieren.

Und es gibt auch WinAPI Wrapper für C#, vielleicht würdest du dich damit auch einfacher tun: https://github.com/prasannavl/WinApi

O
OXO Themenstarter:in
86 Beiträge seit 2020
vor 3 Jahren

Kann man das Control dazu bringen, sich neu zu initialisieren?

Oder eine andere Idee: Kann man irgendwie einen Message-Hook auf das ListView einrichten, so dass man vor dem Einfügen das Item manipulieren kann und dessen Text abändern kann, so dass das so an das Control übergeben wird?

Für diese Variante, würde man da auch ein Override auf das WndProc des eigenen Forms machen und kann sich irgendwie für bestimmte Nachrichten des entsprechenden ListViews registrieren?

4.931 Beiträge seit 2008
vor 3 Jahren

Puh, da hast du dir aber was vorgenommen.

Ob man die A- oder W-Variante beim Marshalling nehmen muß, hängt von den zu benutzenden Funktionen der DLL (bzw. in deinem Fall: des anderen Prozesses ab). Moderne Programme sollten eigentlich UNICODE-fähig sein und damit sollte man die W-Variante benutzen.

Ich denke auch, so wie JimStark, daß du mit dem Ändern des ListView-Texts so nicht Erfolg haben wirst.
Ich hatte gestern schon mal, bezogen auf deinen anderen Beitrag, mit dem Process Monitor versucht rauszufinden, wo die Daten für die Anzeige im "Windows-Fax- und -Scan" (WPS.exe) herkommen, aber die Zugriffsliste ist sehr lang (viele Registry-Zugriffe und diverse DLLs) - da müßte man noch weiter filtern... - das wäre m.E. der beste Weg, wenn man direkt die Daten (Datei, DB, Registry, ...) ändern würde.

Die Methode SendMessageComplexGeneric scheint m.E. so nicht funktionieren zu können (basierend auf SendMessageString aus dem Link?), denn du müßtest ja irgendwie in der (generischen) Methode auf den Member pszText zugreifen.

PS: Der WinAPI-Wrapper unterstützt bisher aber noch keine ListView 😭

Edit: Ich habe jetzt mal selber "Windows Fax- und -Scan"-Programm benutzt und (so wie es scheint) nimmt es einfach die Bild-Dateien (entsprechend bestimmter Filter) aus dem Ordner "Documents\Scanned Documents" und zeigt dessen Infos an.

Für ein eigenes Bild, das ich dort reinkopiert habe, hat es mir dann als "Scandatum" das Erstellungsdatum der Datei und für die Quelle "Unbekanntes Fabrikat (Unbekannte Quelle)" angezeigt.
Und da man diese Daten in dem Programm auch nicht editieren kann, führt daher ein Ändern dieser Werte (per SendMessage) auch nur zur temporären Änderung.

O
OXO Themenstarter:in
86 Beiträge seit 2020
vor 3 Jahren

Puh, da hast du dir aber was vorgenommen.

Ob man die A- oder W-Variante beim Marshalling nehmen muß, hängt von den zu benutzenden Funktionen der DLL (bzw. in deinem Fall: des anderen Prozesses ab). Moderne Programme sollten eigentlich UNICODE-fähig sein und damit sollte man die W-Variante benutzen.

Okay, vermutlich bei den meisten DLLs unter Windows 10 dann schon der Fall.

Beim Umgang mit der WinAPI fällt mir momentan eigentlich am Schwersten, die richtigen Strukturen und Datentypen, sowie deren Werte für die Konstanten für C# irgendwo her zu bekommen.

Wie gehst Du denn da genau vor? In der MSDN findet man ja meist die Structs für C++, teils mit irgendwelchen Unter-Strukturen und Elemente.

Die Frage ist für mich immer, wie bastel ich das für C# richtig hin?

Die simplen Datentypen sind ja kein Thema, aber bereits bei den Strings scheint man komische Pfade zu betreten, wie z.B. diese Geschichte, dass diese in einem anderen Prozess liegen müssen.

Vielleicht als kleine Lehrstunde für mich, würde mich interessieren, wie Ihr mit den Daten aus der MSDN arbeitet, damit das unter C# zum Erfolg wird.

Ich denke auch, so wie JimStark, daß du mit dem Ändern des ListView-Texts so nicht Erfolg haben wirst.
Ich hatte gestern schon mal, bezogen auf deinen anderen Beitrag, mit dem
>
versucht rauszufinden, wo die Daten für die Anzeige im "Windows-Fax- und -Scan" (WPS.exe) herkommen, aber die Zugriffsliste ist sehr lang (viele Registry-Zugriffe und diverse DLLs) - da müßte man noch weiter filtern... - das wäre m.E. der beste Weg, wenn man direkt die Daten (Datei, DB, Registry, ...) ändern würde.

Im Grunde kommen diese Daten aus den Dateinamen auf der Festplatte. Also das Standdard-Verzeichnis ist erst einmal "C:\Users&lt;User>\Documents\Scanned Documents". Von hier liest das Beim Starten die Dateinamen ein.

Vielleicht kann man ja auch das ListView dazu bringen, dass es dies wieder tut?

Die Methode SendMessageComplexGeneric scheint m.E. so nicht funktionieren zu können (basierend auf SendMessageString aus dem Link?), denn du müßtest ja irgendwie in der (generischen) Methode auf den Member pszText zugreifen.

PS: Der WinAPI-Wrapper unterstützt bisher aber noch keine ListView 😭

Die generische Methode arbeitet da ja mit den Strukturen. Diese haben zunächst einen Datentyp "string". Dieser wird im Ziel-Prozess in einem Speicherbereich alloziiert und man hat den Pointer darauf. Also meine Annahme war, dass das auch wirklich funktionieren könnte und wie aus der Struktur gefordert ein lpszText im Ziel-Speicher ist. So viel zur Theorie 😃

Eine weitere Idee, die aber etwas von hinten durch die Brust ins Auge ist: Mir ist aufgefallen, dass wenn ich in "Windows Fax- und -Scan" in dessen TreeView einen Ordner anlege und darauf klicke, das ListView ja erst einmal geleert wird. Benenne ich dann eine Datei auf der Festplatte um und klicke im TreeView wieder auf den Root-Knoten, dann wird im ListView der geänderte Dateiname angezeit!

Die Frage wäre, ob man sich das Zunutze machen kann:
Im TreeView (und vielleicht vorher auf der Platte) einen Dummy-Ordner anlegen, diesen dann per Button-Click anzuklicken (daraufhin ist das ListView leer). Datei auf der Platte umbenennen und danach per Button-Klick irgendwie wieder den Root-Knoten im TreeView selektieren. Dummy-Ordner im TreeView und der Platte wieder löschen.

Leider hab ich es noch nicht geschafft, per Win32-Api nen Ordner im TreeView unter dem Root-Knoten anzulegen 😦

4.931 Beiträge seit 2008
vor 3 Jahren

Du möchtest also die ListView-Anzeige aktualisieren. Leider unterstützt das Programm kein "Aktualisieren" (F5), wie man es vom Windows-Explorer oder anderen Programmen kennt, aber ein Reselektieren des TreeView-Eintrags müßte dazu ausreichen, s. TVM_SELECTITEM sowie TVM_GETNEXTITEM (mit TVGN_CARET als Parameter), d.h.


// Pseudocode
IntPtr selectedItem = SendMessage(TVM_GETNEXTITEM, TVGN_CARET);
SendMessage(TVM_SELECTITEM, IntPtr.Zero);
SendMessage(TVM_SELECTITEM, selectedItem);

O
OXO Themenstarter:in
86 Beiträge seit 2020
vor 3 Jahren

Du möchtest also die ListView-Anzeige aktualisieren. Leider unterstützt das Programm kein "Aktualisieren" (F5), wie man es vom Windows-Explorer oder anderen Programmen kennt, aber ein Reselektieren des TreeView-Eintrags müßte dazu ausreichen, s.
>
sowie
>
(mit TVGN_CARET als Parameter), d.h.

  
// Pseudocode  
IntPtr selectedItem = SendMessage(TVM_GETNEXTITEM, TVGN_CARET);  
SendMessage(TVM_SELECTITEM, IntPtr.Zero);  
SendMessage(TVM_SELECTITEM, selectedItem);  
  

Ja super, jetzt geht das, was ich machen wollte wohl. Als ich händisch Ordner selektiert hatte und zurück gegwechselt bin, wurde wohl erst alles aus dem Cache für das ListView bezogen.

So funktioniert das Anzeigen nach dem Umbenennen anscheinend, wenn ich die Selektion wie folgt weg nehme:


// Werden nicht alle gebraucht
const int TV_FIRST = 0x1100;
const int TVGN_ROOT = 0x0;
const int TVGN_NEXT = 0x1;
const int TVGN_CHILD = 0x4;
const int TVGN_FIRSTVISIBLE = 0x5;
const int TVGN_NEXTVISIBLE = 0x6;
const int TVGN_CARET = 0x9;
const int TVM_SELECTITEM = (TV_FIRST + 11);
const int TVM_GETNEXTITEM = (TV_FIRST + 10);
const int TVM_GETITEM = (TV_FIRST + 12);

string windowTitle = "Windows-Fax und -Scan";
IntPtr ptrWnd = FindWindow(null, windowTitle);
IntPtr ptrWndMDIWindow = FindWindowEx(ptrWnd, IntPtr.Zero, "AfxMDIFrame42u", null);
IntPtr ptrWndNestedMDIWindow1 = FindMDIWindowByIndex(ptrWndMDIWindow, 1);
IntPtr ptrWndFrameOrView = FindWindowEx(ptrWndNestedMDIWindow1, IntPtr.Zero, "AfxFrameOrView42u", null);
IntPtr ptrWndTreeView = FindWindowEx(ptrWndFrameOrView, IntPtr.Zero, "SysTreeView32", null);

// Remove Selection and select Root
var selectionRemovedResult = SendMessage(ptrWndTreeView, TVM_SELECTITEM, TVGN_CARET, 0);