Jakiś czas temu na forum śp. portalu developers.pl zadałem pytanie dotyczące obsługi schowka w aplikacjach Windows Forms. Konkretnie chodziło mi o aplikację MDI, gdzie okna są hostowane w aplikacji głównej i muszą w jakiś sposób przekazywać między sobą informacje za pomocą schowka Windows. Odzew był żaden i w końcu nad problemem musiałem zastanowić się sam. Poniżej prezentuję swoje przemyślenia i czekam na Wasze komentarze.
O cóż dokładnie chodzi? Okno dokumentu musi potrafić reagować na polecenia menu Edit, czyli Copy, Paste, Cut itd. Sam dokument musi potrafić rozpoznać, czy zawartość aktywnej kontrolki nadaje się do skopiowania oraz czy dane, które znajdują się w schowku mogą zostać do kontrolki wklejone. Co więcej, aplikacja sterująca nic nie wie o "aktywnej kontrolce", polecenia wysyłane są do aktywnego dokumentu (rozpoznawanego po właściwości ActiveMdiForm).
Samą architekturę aplikacji opiszę w innym artykule, istotne są następujące informacje:
- Hostowana kontrolka implementuje interfejs IMenuClient, zdefiniowany na listingu poniżej.
- Aplikacja hostująca "pyta" co jakiś czas aktywne okno MDI (jeśli taki istnieje) o obsługę odpowiednich poleceń za pomocą metody IMenuClient .GetItemStatus().
- Efektem wybrania odpowiednie opcji z menu jest wywołanie funkcji IMenuClient.PerformItemAction().
public enum SupportedMenuItem
{
Edit_Copy,
Edit_Paste,
Edit_SelectAll
}
public interface IMenuClient
{
bool GetItemStatus(SupportedMenuItem item, object data);
bool PerformItemAction(SupportedMenuItem item, object data);
}
Teraz pozostaje tylko problem, żeby w miarę inteligentnie sprawdzić czy aktywna kontrolka posiada zawartość nadającą się do skopiowania do schowka, bądź też zawartość schowka nadaje się do wklejenia do niej. Do tego celu zdefiniowałem sobie następujący interfejs:
public interface IClipboardProvider
{
Control Control { get; set;}
bool CanCopy { get; }
bool CanPaste { get; }
bool CanSelectAll { get; }
void Copy();
void Paste();
void SelectAll();
}
Właściwość Control to kontrolka, dla której provider zostaje zdefiniowany. Nazwy odpowiednich metod mówią same za siebie. Dla zwięzłości tekstu pominąłem operacje Clear i Cut.
Przykładowy provider dla kontrolki TextBox, implementujący powyższy interfejs, wygląda następująco:
public class TextBoxClipboardProvider : IClipboardProvider
{
private TextBoxBase ctl;
public Control Control
{
get { return ctl; }
set { ctl = value as TextBoxBase; }
}
public bool CanCopy
{
get { return ctl.SelectionLength != 0; }
}
public bool CanPaste
{
get
{
if (ctl.ReadOnly || !ctl.Enabled)
return false;
return Clipboard.ContainsText(TextDataFormat.Text);
}
}
public bool CanSelectAll
{
get { return true; }
}
public void Copy()
{
Clipboard.SetText(ctl.SelectedText, TextDataFormat.Text);
}
public void Paste()
{
string text = Clipboard.GetText(TextDataFormat.Text);
ctl.SelectedText = text;
ctl.SelectionStart += ctl.SelectionLength;
ctl.SelectionLength = 0;
}
public void SelectAll()
{
ctl.SelectionStart = 0;
ctl.SelectionLength = ctl.TextLength;
}
}
Jako typu kontrolki użyłem TextBoxBase, dzięki czemu klasa ta będzie obsługiwać wszystkie kontrolki bazujące na TextBoxie.
Nieco bardziej skomplikowana jest definicja providera dla kontrolki DataGridView: przy wklejaniu trzeba uważać, żeby dane zmieściły się w granicach grida, a także należy sprawdzić czy logika kontrolki pozwala na wklejenie odpowiednich danych (np. należy uważać na komórki przeznaczone tylko do odczytu). Samą definicję klasy dołączam do artykułu.
Pozostaje w tej chwili zgranie napisanych klas providerów z kontrolką potrzebującą obsługi schowka. Istotne jest to, że potrzebujemy informacje o obsłudze schowka dla konkretnej kontrolki, a nie dla typu kontrolkę reprezentującego. Z drugiej strony zastosowanie mechanizmu pollingu implikuje częste pytania (do kilku razy na sekundę), co wymaga przechowywania informacji o konkretnej kontrolce – tworzenie nowego providera mogłoby być zbyt kosztowne, szczególnie, że potrzebujemy kilku informacji o kontrolce (obsługa Copy, Paste, …) . Należy też unikać martwych referencji – jeśli okno zawierające kontrolkę zostanie zamknięte, to muszą zostać zwolnione informacje o klasach obsługujących kontrolki tego formularza.
Dla przechowywania informacji o providerach kontrolek można wzorca Factory:
public static class ClipboardHelper
{
private delegate IClipboardProvider GetProviderDelegate();
private static Dictionary<Control, IClipboardProvider> dictionary;
private static Dictionary<Type, GetProviderDelegate> providers;
public static IClipboardProvider GetClipboardProvider(Control ctl)
{
if (dictionary == null)
InitDictionary();
if (dictionary.ContainsKey(ctl))
return dictionary[ctl];
if (providers == null)
InitProviders();
Type t = ctl.GetType();
while (!providers.ContainsKey(t) && t.BaseType != null)
t = t.BaseType;
if (providers.ContainsKey(t))
{
IClipboardProvider provider = providers[t]();
provider.Control = ctl;
dictionary.Add(ctl, provider);
return provider;
}
return null;
}
public static void ClearControlData(Control parent)
{
foreach (Control ctl in new List<Control>(dictionary.Keys))
{
Control _ctl = ctl.Parent;
while (_ctl != null)
{
if (_ctl == parent)
{
dictionary.Remove(ctl);
break;
}
_ctl = _ctl.Parent;
}
}
}
private static void InitDictionary()
{
dictionary = new Dictionary<Control, IClipboardProvider>();
}
private static void InitProviders()
{
providers = new Dictionary<Type, GetProviderDelegate>();
providers.Add(typeof(TextBoxBase),
delegate() { return new TextBoxClipboardProvider(); });
providers.Add(typeof(DataGridView),
delegate() { return new DataGridViewClipboardProvider(); });
}
}
W aplikacji założyłem sobie, że elementem implementującym interfejs IMenuClient jest kontrolka, stąd metoda ClearControlData(). Mechanizm aplikacji opiszę, jak już wspomniałem, kiedy indziej.
Sama obsługa schowka staje się w tym momencie bajecznie prosta. Należy pamiętać tylko o dwóch rzeczach:
- W metodzie Dispose() kontrolki należy wywołać ClipboardHelper.ClearClipboardData(this);
- Zawartość właściwości ActiveControl nie zawsze stanowi kontrolka aktualnie posiadająca fokus. Jeśli znajduje się ona w kontenerze (np. w panelu), to kontrolką aktywną będzie właśnie ten panel, który także posiada własność ActiveControl.
Sam kod dla funkcji GetItemStatus() wygląda następująco:
private Control ActiveUserControl
{
get
{
Control ctl = ActiveControl;
while (ctl is IContainerControl)
ctl = ((IContainerControl)ctl).ActiveControl;
return ctl;
}
}
public bool GetItemStatus(SupportedMenuItem item, object data)
{
IClipboardProvider cp = ClipboardHelper.GetClipboardProvider(ActiveUserControl);
switch (item)
{
case SupportedMenuItem.Edit_Copy:
return cp == null ? false : cp.CanCopy;
case SupportedMenuItem.Edit_Paste:
return cp == null ? false : cp.CanPaste;
case SupportedMenuItem.Edit_SelectAll:
return cp == null ? false : cp.CanSelectAll;
}
return false;
}
Analogicznie wygląda metoda PerformItemAction().
I to tyle. Z rozwiązania jestem zadowolony, bo przede wszystkim działa. Ciekaw jednak jestem Waszych opinii na ten temat.