Temat: Lista wypunktowana w WinForms

Jeśli ktoś kiedyś potrzebował listy wypunktowanej w aplikacji formsowej, a TreeView odpadało (np. ze względu na brak możliwości zawijania długiego tekstu) i męczył się z pozycjonowaniem labeli (zwłaszcza przy opcji zawijania tekstu przy resize formatki - ja to przeszedłem), to może przyda mu się następujące rozwiązanie:

1. Przygotować klasę kontenera, która będzie przechowywać elementy listy i implementuje renderer listy, zapisujący listę wypunktowaną w HTMLu w zmiennej tekstowej
2. Podać zawartość zmiennej do kontrolki przeglądarki
3. Dopasować rozmiar kontrolki do contentu, jeśli potrzeba
4. Oprogramować kliknięcia na elementach listy, jeśli potrzeba.

Efekt końcowy:
Obrazek

Jak widać, kliknięcie na pozycji listy generuje zdarzenie.

Zakładam dla prostoty (tak mi było potrzebne), że każda następna sublista jest głębiej od poprzednich i nie ma "powrotów", tzn:
111
222
333
444
555
666
777

zamiast
111
222
333
444
555
666
777

Jak się chce mieć ten drugi wariant, trzeba odrobinę pokombinować przy renderowaniu listy, napisać metodę rekurencyjną, która będzie dopisywać pozycje listy na odpowiednim poziomie. Mnie to nie było potrzebne.

Kody

1. Ustawienia kontrolki przeglądarki
public Form1()
{
InitializeComponent();
// ...
this.webBrowser1.WebBrowserShortcutsEnabled = false;
this.webBrowser1.AllowWebBrowserDrop = false;
this.webBrowser1.Capture = false;
this.webBrowser1.ContextMenu = null;
this.webBrowser1.AllowNavigation = false;
this.List = new ICD10List(new Font("Verdana", 11));
}


2. Dodawanie pozycji do listy. Liczby (1111, 2222) to jakieś tam fikcyjne identyfikatory, skojarzone z etykietami. Np. po to, aby po kliknięciu, można było coś wyciągnąć z bazy.
private void button1_Click(object sender, EventArgs e)
{
this.List.AddSubList();
this.List.AddSubListItem("(C00-D48) Nowotwory", 11111);

this.List.AddSubList();
this.List.AddSubListItem("(C00-C75) Nowotwory złośliwe o określonym umiejscowieniu uznane lub podejrzane jako pierwotne, za wyjątkiem nowotworów tkanki limfatycznej, krwiotwórczej i tkanek pokrewnych:", 222);

this.List.AddSubList();
this.List.AddSubListItem("(C00-C14) Wargi, jamy ustnej i gardła", 3333);

this.List.AddSubList();
this.List.AddSubListItem("(C00.5) Powierzchnia wewnętrzna wargi nieokreślonej (górna lub dolna)", 444444444, true);
// true = na tym elemencie kończy się dokument.
// Do obliczenia wysokości kontrolki.

// montujemy listę :]
this.webBrowser1.DocumentText = this.List.MakeList();


// jeśli trzeba obsłużyć kliknięcia
this.webBrowser1.Document.Click += new HtmlElementEventHandler(Document_Click);


// kontrolka rysuje asynchronicznie. Można to załatwić eventem, ale tutaj to przerost formy nad treścią.
while (this.webBrowser1.ReadyState != WebBrowserReadyState.Complete)
{
Application.DoEvents();
}


// ustawienie wysokości kontrolki, jesli trzeba
this.webBrowser1.Height = this.webBrowser1.Document.GetElementById(this.List.LastID.ToString()).OffsetRectangle.Bottom + 20;


3. Obsługa kliknięcia
void Document_Click(object sender, HtmlElementEventArgs e)
{
HtmlElement link = this.webBrowser1.Document.ActiveElement;

// chcę reagować tylko na klikanie "klikacza".
// Zdarzenie odpalane jest dla każdego kliknięcia w obszarze dokumentu
if (link.GetAttribute("klikacz") == "1")
MessageBox.Show(link.GetAttribute("id"));
}


4. I wreszcie definicja klasy ICD10List (każdy sobie wymyśli własny kontener na pozycje listy. Mój jest quick'n'dirty) wraz z renderowaniem listy.
public class ICD10List
{
// element listy ma nazwę i ID
struct ItemDescription
{
public readonly int iID;
public readonly string sName;

public ItemDescription(int ID, string Name)
{
this.sID = ID;
this.iName = Name;
}
}

string sHtmlHeader;
string sHtmlFooter;
string sList;
int iLevel;
Font fFont;
int iLastID;

// Struktura listy
Dictionary<int, List<ItemDescription>> SubLists;

public ICD10List(Font Font)
{
this.SubLists = new Dictionary<int, List<ItemDescription>>();
this.sHtmlHeader = "<html><body>";
this.sHtmlFooter = "</body></html>";

this.fFont = Font;
}

public void AddSubList()
{
if (!this.SubLists.ContainsKey(this.iLevel))
{
this.SubLists.Add(this.iLevel, new List<ItemDescription>());
this.iLevel++;
}
}

public int LevelCount
{
get { return this.iLevel; }
}

public int LastID
{
get { return this.iLastID; }
}

public void AddSubListItem(string Item, int ItemID)
{
if (this.SubLists.ContainsKey(SubListLevel-1))
{
this.SubLists[SubListLevel-1].Add(new ItemDescription(ItemID, Item)) ;
}
}

public void AddSubListItem(string Item, int ItemID, bool LastID)
{
AddSubListItem(Item, ItemID);
this.iLastID = ItemID;
}

// marginesy można oczywiście sparametryzować
public string MakeList()
{
this.sList = this.sHtmlHeader;

for (int i = 0; i < this.iLevel; i++)
{
this.sList += "<ul style=\"font-family:" + this.fFont.Name + "; font-size:" + this.fFont.Size + "px; margin-top:15px; margin-left:20px;\">";

for (int j = 0; j < this.SubLists[i].Count; j++)
{
// dodajemy linki, nadajemy im IDki z bazy, dajemy im atrybut klikacz (równy 1) i ew. zamieniamy w tekście znaki nowej linii na BeeRy.

this.sList += "<li><a klikacz='1' id=" + this.SubLists[i][j].ID + " href='' style='filter:none;'>" + this.SubLists[i][j].Name.Replace(Environment.NewLine, "<br>") + "</a>" + "</li>";
}
}

for (int i = 0; i < this.iLevel; i++)
{
this.sList += "</ul>";
}

return this.sList += this.sHtmlFooter;
}
}

Można dodatkowo pozbyć się ramek przeglądarki i osadzić w innej kontrolce. Można wyrenderować content do bitmapy i wstawić do PictureBoxa, jak komu wygodniej. Przypominam, że tak się świetnie renderuje indeksy (potęgi, indeksy stechiometryczne) poprzez <sup> i <sub>.Adrian Olszewski edytował(a) ten post dnia 22.10.09 o godzinie 20:17