Krzysztof Radzimski

Pasja tworzenia

Mutex, COPYDATASTRUCT, WM_COPYDATA – Application file handling

13 październik
Opublikował Krzysztof Radzimski 13. października 2011 10:13

Bardzo często, przynajmniej w moim przypadku istnieje potrzeba obsługi jakiś plików w aplikacji.Najogólniej chodzi o taki rodzaj aplikacji, która w jednej instancji obsługuje otwieranie wielu plików. W takim wypadku nie ma potrzeby otwierania wielu instancji aplikacji. O ile wykonanie takiej aplikacji w warstwie obsługi takich plików za pomocą standardowych okien OpenFileDialog i SaveFileDialog nie nastręcza żadnych problemów, o tyle obsługa tych plików z poziomu eksploratora Windows owszem. Oczekiwana dla każdego funkcjonalność polega na możliwości otwarcia pliku za pomocą przyciągania pliku na ikonę aplikacji lub dwuklik pliku, jeżeli nasza aplikacja jest skojarzona z tym typem pliku.

Poniżej testuję sprawdzone podejście na Windows 7 x64 z Visual Studio 2010.

Najpierw zaczniemy od paru niezbędnych funkcji z biblioteki User32.dll
Do tego celu tworzymy sobie klasę.

namespace Abc {
  using System;
  using System.Drawing;
  using System.Collections;
  using System.ComponentModel;
  using System.Windows.Forms;
  using System.Data;
  using System.IO;
  using System.Runtime.InteropServices;
  using System.Text;
  /// <summary>
  /// Win32 API declarations
  /// </summary>
  public class Win32 {
    public const int WM_COPYDATA = 0x4A;
    // A delegate to pass to the EnumWindows API call as the Callback Method
    public delegate int EnumWindowsProc(IntPtr hwnd, int lParam);
    [DllImport("user32.dll")]
    public static extern long SendMessage(
      IntPtr hWnd,
      uint Msg,
      uint wParam,
      ref COPYDATASTRUCT lParam
      );
    [DllImport("user32")]
    public extern static IntPtr GetProp(
      IntPtr hwnd,
      string lpString);
    [DllImport("user32")]
    public extern static int SetProp(
      IntPtr hwnd,
      string lpString,
      int hData);
    [DllImport("user32")]
    public extern static IntPtr RemoveProp(
      IntPtr hwnd,
      string lpString);
    [DllImport("user32")]
    public static extern int GetWindowText(
      int hwnd,
      StringBuilder
      lpString,
      uint bufferSize);
    [DllImport("user32.dll")]
    public static extern int EnumWindows(
      EnumWindowsProc callback,
      int lParam);
    [DllImport("User32.dll", EntryPoint = "FindWindow")]
    public static extern Int32 FindWindow(String lpClassName, String lpWindowName);
    [DllImport("User32.dll", EntryPoint = "SetForegroundWindow")]
    public static extern bool SetForegroundWindow(int hWnd);
    /// <summary>
    /// COPYDATASTRUCT for holding the data passed using WM_COPYDATA
    /// </summary>
    [StructLayout(LayoutKind.Sequential)]
    public struct COPYDATASTRUCT {
      public IntPtr dwData;
      public int cbData;
      public IntPtr lpData;
    }
  }
}

Teraz tworzymy Mutex (nie będę pisał co to jest :) )
Mutex tworzymy przy starcie aplikacji w funkcji Main.

    /// <summary>
    /// Creates the application mutex.
    /// </summary>
    /// <returns></returns>
    public static bool CreateApplicationMutex() {
      bool createdNew = false;
      try {
        ApplicationMutex = Mutex.OpenExisting(APP_ID);
      }
      catch { }
      if (ApplicationMutex != null) {
        createdNew = false;
      }
      else {
        ApplicationMutex = new Mutex(true, APP_ID, out createdNew);
      }
      return createdNew;
    }
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static int Main(string[] args) {
 if (CreateApplicationMutex()){
    // normalne uruchomienie aplikacji
 }else {
    // wykonanie operacji otwarcia pliku w
    // już otwartej instancji aplikacji 
 }
}

Kod który mieliśmy w funkcji Main umieszczamy po spełnieniu warunku, a w przeciwnym przypadku musimy wysłać komunikat do otwartego głównego okna aplikacji ze ścieżką pliku i co oczywiste otworzyć go.

Stała APP_ID to wymyślony GUID, który pełni rolę unikalnego identyfikatora aplikacji.

Wracając do naszego komunikatu, musimy przedtem wykonać parę operacji. Po pierwsze jeżeli okno jest zminimalizowane do paska zadań to trzeba je przywrócić, tak czy inaczej pokazać jeżeli inne okna je zakrywają. Tu posługujemy się paroma funkcjami z User32.dll

/// <summary>
/// Czy okno jest w pasku zadań?
/// </summary>
/// <param name="hWnd">Uchwyt okna.</param>
/// <returns></returns>
[DllImport("user32.dll")]
public static extern bool IsIconic(IntPtr hWnd);
/// <summary>
/// Przynosi wątku, który utworzył określonym oknie na pierwszym planie i aktywuje okno. klawiatura jest skierowana do okna, i różne wzrokowych są zmieniane przez użytkownika. System przypisuje nieznacznie wyższy priorytet wątku, który utworzył okno na pierwszym planie, niż to do innych wątków.
/// </summary>
/// <param name="hWnd">Uchwyt do okna, które powinny być włączane i przeniesione na pierwszy plan.</param>
/// <returns></returns>
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool SetForegroundWindow(IntPtr hWnd);
/// <summary>
/// Pokazuje okno.
/// </summary>
/// <param name="hWnd">Uchwyt okna.</param>
/// <param name="nCmdShow">Komenda.</param>
/// <returns></returns>
[DllImport("user32.dll")]
private static extern int ShowWindow(IntPtr hWnd, int nCmdShow);
// czy zminimalizowane?
if (IsIconic(process.MainWindowHandle)) {
  // przywrocenie okna.
  ShowWindow(process.MainWindowHandle, SW_RESTORE);
}
// pokaż na wierzchu.
SetForegroundWindow(process.MainWindowHandle);

Teraz można przystąpić do wysłania komunikatu ze ścieżką pliku.

// wykonanie argumentów wywołania.
if (args.Length > 0) {
  GetRunningInstance();
  IntPtr ptr = Marshal.StringToHGlobalAnsi(args[0]);
  var cds = new Abc.Win32.COPYDATASTRUCT();
  cds.dwData = IntPtr.Zero;
  cds.cbData = Environment.CommandLine.Length;
  cds.lpData = ptr;
  long result = Abc.Win32.SendMessage(prevPtr, Abc.Win32.WM_COPYDATA, 0, ref cds);
  // Required to free unmanaged memory otherwise a memory leak occurs
  Marshal.FreeHGlobal(ptr);
}
...
private static void GetRunningInstance() {
  // Enumerate windows
  var enumWindowsProc = new Abc.Win32.EnumWindowsProc(WindowEnumProc);
  Abc.Win32.EnumWindows(enumWindowsProc, 0);
}
private static int WindowEnumProc(IntPtr hWnd, int lParam) {
  StringBuilder sb = new StringBuilder(255);
  IntPtr propPtr = Abc.Win32.GetProp(hWnd, APP_ID);
  if (propPtr.ToInt32() != 0) {
    prevPtr = propPtr;
    return 0;
  }
  return 1;
}

Warto zwrócić uwagę na metodę GetRunningInstance. Na podstawie id aplikacji, o którym pisałem wcześniej, wyszukujemy uchwyt głównego okna aplikacji. Jest on następnie użyty w wywołaniu metody SendMessage. Na tym etapie proces wysłania komunikatu zawierającego COPYDATASTRUCT mamy zakończony. Teraz ten komunikat musimy odebrać w głównym oknie aplikacji za pomocą metody WndProc.

Wcześniej jednak w obsłudze zdarzenia Load dodajemy kawałek kodu służącego do ustawienia ID aplikacji

    void MainForm_Load(object sender, EventArgs e) {
      try {
        if (Abc.Win32.SetProp(this.Handle, Program.APP_ID, (int)this.Handle) == 0) {
          throw new ApplicationException("Nie można ustawić Id aplikacji.");
        }
...
Oczywiście przy zamknięciu okna musimy wykonać operację odwrotną.
    private void MainForm_FormClosing(object sender, FormClosingEventArgs e) {
      try {
        Abc.Win32.RemoveProp(this.Handle, Program.APP_ID);
....
Teraz można obsłużyć WndProc
protected override void WndProc(ref Message msg) {
      try {
        switch (msg.Msg) {
          case Abc.Win32.WM_COPYDATA: {
              // Tworzymy strukturę
              string strCmdLine = string.Empty;
              var cds = new Abc.Win32.COPYDATASTRUCT();
              cds = (Abc.Win32.COPYDATASTRUCT)System.Runtime.InteropServices.Marshal.PtrToStructure(
                msg.LParam, typeof(Abc.Win32.COPYDATASTRUCT));
              if (cds.cbData > 0) {
                // pobieramy ścieżkę pliku 
                strCmdLine = System.Runtime.InteropServices.Marshal.PtrToStringAnsi(cds.lpData);
                if (!String.IsNullOrEmpty(strCmdLine)) {
                  if (File.Exists(strCmdLine)) {
                    this.Activate();
                    // Otwieram plik
                    open(strCmdLine);
                  }
                }
              }
              break;
            }
        }
      }
      catch (Exception ex) {
        // możemy zapisać do jakiegoś logu
        // ale nie wyświetlamy tu MessageBox-a
      }
      // obsługa pozostałych komunikatów
      base.WndProc(ref msg);
    }

I to jest dokładnie wszystko.

Pozostaje jeszcze obsługa Drag&Drop .. ale to może innym razem.


 

Tagi: | Kategorie: Programowanie

Wyślij link na adres e-mail | Link do tego postu | RSSRSS comment feed