ListBox, MultiSelect & OwnerDraw

0

Witam wszystkich;

Tworzę program, w których 80% danych programu będzie przechowywanych w jednym komponencie ListBox; Z racji tej postanowiłem stworzyć proste, ale eleganckie GUI; Zaprojektowałem już wszystkie elementy, oprogramowane zostały wszystkie, prócz tego głównego ListBox; Niestety pomysł, na jaki wpadłem przysporzył mi nie lada zagadkę;

Sposób malowania tła:
*każdy item nie zaznaczony powinien mieć białe tło bez obramówki
*każdy zaznaczony item powinien mieć tło niebieskie z czarną obramówką na 1px
Niby nic w tym trudnego, ale nie tak szybko; Otóż każdy item, który jest zaznaczony powinien mieć czarną obwódkę o szerokości jednego piksela, ale jest jeden warunek - jeżeli sąsiedni item też jest zaznaczony, powinny mieć wspólne tło i wspólną - jedną ramkę;

Przykład łączenia tła i ramki w itemach (WinXP + XPManifest)
563ebb487fcd.png

Wszystko odnośnie samego malowania wiem, w jaki sposób ustawiać różne rzeczy, malować różne rzeczy także, ale za cholere nie wiem, w jaki sposób zaprogramować, by sąsiednie itemy miały wspólne tło i ramkę;

Czy ktoś wie, w jaki sposób można to rozwiązać? Na co mam zwrócić uwagę? Nie potrzebuję kodu, chodzi mi o teorię (pomysł), kod napiszę sam;

Bardzo proszę o pomoc; Serdecznie dziękuję za zainteresowanie;

PS: Nie wrzucam kodu, bo do tego co potrzebuję nie mam ani jednej linijki; Na razie chcę się dowiedzieć jak to w teorii wygląda; Jak ktoś żywcem chce obejżeć kod to zapodam jakikolwiek przykładowy;

0

jesli jestem zaznaczony to:
sprawdzam czy sasiad z gory jest zaznaczony. jesli tak to nie rysuję gornej ramki (pod warunkiem ze nie jestem pierwszym itemem listbox).
jesli sasiad z dolu jest zaznaczony nie rysuję dolnej ramki (chyba, ze jestem ostatnim itemem listbox).

0

Dobrze cimak, słuszna uwaga - też na nią wpadłem; Niestety w ten sposób kontroluje się tylko aktualnie rysowany item; Faktycznie na malowanym itemie nie ma górnej części ramki, ale jest problem z sąsiadem;

Dajmy na to - itemy o indeksach 2 i 3 są zaznaczone; W metodzie DrawItem parametr Index przechowuje wartość 3; Jeżeli podczas sprawdzania warunek wykryje, że poprzedni item (o indeksie 2) też jest zaznaczony - nie narysuje górnej części ramki; Niestety dolna część ramki w itemie o indeksie 2 została wcześniej namalowana i ona zostanie; A jeżeli chciałbym wywołać dla tego itemu metodę DrawItem - powstanie niekończąca się rekurencja...

0

czekaj.. w momencie rysowania "2" wiesz ze "3" jest zaznaczony wiec nie rysujesz dolnej ramki (wiedzac, ze za chwile "3" nie narysuje gornej)... ?
hmm... w sumie... mozna napotkac maly problem podczas zaznaczania, faktycznie. ale wystarczy dac invalidate czy tam repaint i problem znika.

0

może podczas rysowania wyjeżdżaj poza wyznaczony Rect-em obszar o jeden piksel w górę (kilkukrotnie rysując lub marząc tą samą linię).

0
cimak napisał(a)

Ale wystarczy dac invalidate czy tam repaint i problem znika.

Próbowałem z Repaint, Refresh, jednym słowem %$#^&... W którym miejscu proponujesz użyć Repaint?

Z zaznaczaniem jest problem, dziwnie się zachowywały itemy; Nie mam zielonego pojęcia jak się za to zabrać, a niemożliwe wręcz jest to, by to było nie do zaprogramowania;

krwq napisał(a)

może podczas rysowania wyjeżdżaj poza wyznaczony Rect-em obszar o jeden piksel w górę (kilkukrotnie rysując lub marząc tą samą linię).

Też próbowałem, metoda Rectangle nie namaluje ani jednego piksela poza obszarem Rect nawet wtedy, gdy wywołam ją tak:

Rectangle(Rect.Left, Rect.Top - 10, Rect.Right, Rect.Bottom + 10)

Teoretycznie w pseudolangłydżu wyglądało by to tak (malując samo tło):

1. Sprawdź, czy obecnie malowany item jest zaznaczony - jeżeli tak
  begin
    1.1. Maluj niebieskie tło z czarną ramką

    1.2. Sprawdź, czy item o indeksie `Index - 1` istnieje - jeżeli tak
      1.2.1. Sprawdź, czy item o indesie `Index - ` jest zaznaczony - jeżeli tak
        begin
          1.2.1.1. Zamaluj górną część ramki
          1.2.1.2. Usuń dolną część ramki w itemie o indeksie `Index - 1`
        end;

    1.3. Sprawdź, czy item o indeksie `Index + 1` istnieje - jeżeli tak
      1.3.1. Sprawdź, czy item o indeksie `Index + 1` jest zaznaczony - jeżeli tak
        begin
          1.3.1.1. Zamaluj dolną część ramki
          1.3.1.2. Usuń dolną część ramki w itemie o indeksie `Index + 1`
        end;
  end
2. Jeżeli nie
  begin
    2.1. Maluj pełne (białe) tło

    2.2. Sprawdź, czy item o indeksie `Index - 1` istnieje - jeżeli tak
      2.2.1. Sprawdź, czy item o indeksie `Index - 1` jest zaznaczony - jeżeli tak
        2.2.1.1. Namaluj dolną część ramki w itemie o indeksie `Index - 1`

    2.3. Sprawdź, czy item o indeksie `Index + 1` istnieje - jeżeli tak
      2.3.1. Sprawdź, czy item o indeksie `Index + 1` jest zaznaczony - jeżeli tak
        2.3.1.1. Namaluj górną część ramki w itemie o indeksie `Index + 1`
  end;

Niestety nie wiem jak wykonać kroki:
*1.2.1.2.
*1.3.1.2.
*2.2.1.1.
*2.3.1.1
;

0

możesz zrobić jakąś flagę, która mówi Ci o tym czy funkcja została wywołana przez Ciebie czy nie przez Ciebie i inaczej zachowywać się w zależności od jej ustawienia.

Ostatecznie możesz napisać ten komponent od zera. narysowanie kilku prostokątów i reakcja na zdarzenia onClick [+CTRL][+SHIFT] nie powinna być zbyt trudna i oszczędzi Ci wiele zmartwień. Możesz też stworzyć nowy komponent który dziedziczy po listboxie i dodaje mu brakujące funkcjonalności

0
Furious Programming napisał(a)

Próbowałem z Repaint, Refresh, jednym słowem %$#^&... W którym miejscu proponujesz użyć Repaint?

w momencie gdy zaznaczysz/odznaczysz jakis item.

0

Nie ma szans - program się dławi generując nieskończoną rekurencję;

EDIT: Może nie nieskończoną, ale jeżeli mam kilkadziesiąt itemów na liście z czego zacznę zaznaczać kilka do jestem pewny, że pętla odświeżająca zawartość itemów dziesiątki razy sprawdza każdy z zaznaczonych itemów i je na nowo odmalowuje, po czy następny, następny i przez to się program zacina;

0

to chyba cos robisz nie tak.

zobacz, dziala:

procedure TForm1.ListBox1Click(Sender: TObject);
begin
 listbox1.Repaint;
end;

procedure TForm1.ListBox1DrawItem(Control: TWinControl; Index: Integer;
  Rect: TRect; State: TOwnerDrawState);
var c:TCanvas;
begin
 c:=ListBox1.Canvas;

 if odSelected in State
  then c.Brush.Color:=clHighlight
  else c.Brush.Color:=clWindow;

 c.FillRect(rect);
 if odSelected in State then
  begin
   c.Pen.Color:=clBlack;
   c.Rectangle(rect);
  end;

 if (odSelected in State) then
  begin
    if (index>0) and (ListBox1.Selected[index-1]) then
      begin
        c.Pen.Color:=clHighlight;
        c.MoveTo(1, rect.top);
        c.LineTo(rect.Right-1, rect.top);
      end;

    if (index<ListBox1.Items.Count-1) and (ListBox1.Selected[index+1]) then
      begin
        c.Pen.Color:=clHighlight;
        c.MoveTo(1, rect.bottom-1);
        c.LineTo(rect.Right-1, rect.bottom-1);
      end;
  end;

 c.TextOut(rect.Left+3, rect.Top+3, ListBox1.Items[index]);

 if odFocused in State
  then c.DrawFocusRect(rect);
end;

kod odczywiscie do poprawienia i skrocenia bo pisany na szybko.
jedynym minusem jest to, ze w czasie zaznaczania item sie podswietla ale linia jest jeszcze przez chwile widoczna (do momentu puszczenia przycisku myszy) ale to tez jest do zrobienia.

ciekawe jest tez to, ze onClick dziala nie tyle na klikniecie co na zaznaczenie itema - dziala wiec takze dla zaznaczania itemow klawiatura.
onClick powinno sie raczej nazywac onSelect...?

0
cimak napisał(a)

kod odczywiscie do poprawienia i skrocenia bo pisany na szybko.

Owszem, postaram się; Czemu tworzysz zmienną c? Ja wolę się odwoływać do obiektu Sender poprzez rzutowanie na typ TCustomListBox sprzężając go z blokiem with do:

with TCustomListBox(Sender) do
  begin
    //dalsze instrukcje
  end;
cimak napisał(a)

jedynym minusem jest to, ze w czasie zaznaczania item sie podswietla ale linia jest jeszcze przez chwile widoczna (do momentu puszczenia przycisku myszy) ale to tez jest do zrobienia.

Może lepiej będzie umieścić Repaint w zdarzeniu OnMouseDown - wtedy od razu odświeży (tyle, że klawiatury nie obsłuży - ale operując strzałkami z każdym wciśnięciem strzałki w górę czy w dół i tak wywoływana jest metoda OnClick AFAIR);

cimak napisał(a)

ciekawe jest tez to, ze onClick dziala nie tyle na klikniecie co na zaznaczenie itema - dziala wiec takze dla zaznaczania itemow klawiatura. onClick powinno sie raczej nazywac onSelect...?

Działa i podczas klikania i wciskania klawiszy, tak że nazwa powinna łączyć je obie - sam jednak nie znam takiego słowa... :)

Sprawdzę kod, ale najpierw skończę to co zacząłem - napiszę jak poszło; Dzięki cimak :)

0
Furious Programming napisał(a)

metoda Rectangle nie namaluje ani jednego piksela poza obszarem Rect nawet wtedy, gdy wywołam ją tak:

Rectangle(Rect.Left, Rect.Top - 10, Rect.Right, Rect.Bottom + 10)

No właśnie po to definiuje się obszar Rect aby poza nim nie rysować.
Ale przed rysowaniem możesz przecież zmienić rozmiar obszaru (InflateRect) czy położenie obszaru (OffsetRect) albo np. tak

//Rect obszar, który podał OnDrawItem
var r:TRect;
  r.Left:=Rect.Left;
  r.Top:=Rect.Top - 10;
  r.Right:=Rect.Right;
  r.Bottom:=Rect.Bottom + 10;
  Rectangle(r);
0
pelsta napisał(a)

Ale przed rysowaniem możesz przecież zmienić rozmiar obszaru (InflateRect) czy położenie obszaru (OffsetRect)

O widzisz, a tego jeszcze nie robiłem :)

Czyli najlepszym sposobem jest stworzenie nowej zmiennej w której będzie się przechowywać zmodyfikowanyRect z parametru;

Nie miałem czasu do tej pory siąść nad programem więc jeszcze nie testowałem, ale jak wrócę z cmentarza to sprawdzę wszystkie podane przez Was kody; Dzięki chłopaki;

EDIT: No, jestem już; Muszę powiedzieć, że jak pisałem ten temat to chyba już za późno było, by cokolwiek urodzić; Teraz posiedziałem dosłownie chwilkę i udało się zrobić prosty, jeszcze zbyt wolny algorym ale już jestem na dobrej drodze; Zrobiłem to łączenie - kod przedstawiam poniżej (malowanie samego tła):

with TCustomListBox(Control).Canvas do
  begin
    { CHECK ITEM SELECTED STATE }
    case odSelected in State of
      { SELECTED }
      True:  begin
               { BACKGROUND }
               Pen.Color := clBlack;
               Brush.Color := clMenuHighlight;

               Rectangle(Rect);

               Pen.Color := clMenuHighlight;

               { CHECK UPPER ITEM EXISTS & SELECTED STATE }
               if Index in [1 .. TCustomListBox(Control).Items.Count - 1] then
                 if TCustomListBox(Control).Selected[Index - 1] then
                   { DELETE ITEMS UPPER COMMOM LINE }
                   Rectangle(Rect.Left + 1, Rect.Top - 1, Rect.Right - 1, Rect.Top + 1);

               { CHECK LOWER ITEM EXISTS & SELECTED STATE }
               if Index in [0 .. TCustomListBox(Control).Items.Count - 2] then
                 if TCustomListBox(Control).Selected[Index + 1] then
                   { DELETE ITEMS LOWER COMMON LINE }
                   Rectangle(Rect.Left + 1, Rect.Bottom - 1, Rect.Right - 1, Rect.Bottom + 1);
             end;

      { UNSELECTED }
      False: begin
               { BACKGROUND }
               Pen.Color := clWhite;
               Brush.Color := clWhite;

               Rectangle(Rect);

               Pen.Color := clBlack;

               { CHECK UPPER ITEM EXISTS & SELECTED STATE }
               if Index in [1 .. TCustomListBox(Control).Items.Count - 1] then
                 if TCustomListBox(Control).Selected[Index - 1] then
                   { ADD ITEMS UPPER COMMON LINE}
                   begin
                     MoveTo(Rect.Left, Rect.Top - 1);
                     LineTo(Rect.Right, Rect.Top - 1);
                   end;

               { CHECK LOWER ITEM EXISTS & SELECTED STATE }
               if Index in [0 .. TCustomListBox(Control).Items.Count - 2] then
                 if TCustomListBox(Control).Selected[Index + 1] then
                   { ADD ITEMS LOWER COMMON LINE }
                   begin
                     MoveTo(Rect.Left, Rect.Bottom);
                     LineTo(Rect.Right, Rect.Bottom);
                   end;
             end;
    end;

    { DELETE OLD FOCUS RECT }
    if odFocused in State then
      begin
        Pen.Color := Pen.Color xor $FFFFFF;
        DrawFocusRect(Rect);
      end;
  end;

Na razie KISS & DRY w nim nie obowiązuje :) Efekt wykorzystania kodu:
GoodList.png

Na pierwszy rzut oka działa, klikając na poszczególne itemy pięknie się łączą, obsługa klawiatury też jest w porządku (same strzałki a także zaznaczając kilka pozycji z shift'em); Wszystko by było dobrze, gdyby nie to, że jak się wciśnie LPM na jednym itemie i przesuwa w górę lub w dół zaznaczając kilka kolejnych - znowu rysowane są linie łączące... Ale dopiero od tego o 2 pozycje wyżej lub niżej tego, nad którym wcisnęło się przycisk;

Żeby to zrozumieć, przedstawiam zrzut programu, na którym widać wadę; Lista ma sześć pozycji - wcisnąłem LPM nad ostatnim i zaznaczając wszystkie przesuwając kursor w górę puściłem LPM nad pierwszym na liście:
BadList.png

Wie ktoś dlaczego tak się dzieje? Ten jeden wyżej malowany jest dobrze, ale kolejne już nie; To tak jakby wykonywała się metoda malująca dla nich, ale bez warunku sprawdzającego to, czy usuwać lub dodawać górną czy dolną cześć ramki...

0

Czy wy wiecie że jest taki komponent jak TCheckListbox?

0
Tom Riddle napisał(a)

Czy wy wiecie że jest taki komponent jak TCheckListbox?

Wiedzieć wiem, z resztą wielokrotnie z niego korzystałem, ale chciałem wiedzieć w jaki sposób zrobić właśnie taki bajer, o jakim napisałem w temacie; Wiem, że prościej było by użyć właśnie tego komponentu, ale chciałbym znać sposób na zrobienie właśnie takiego efektu, który bardzo mi się podoba, niestety którego nie udało mi się zrobić mimo wielu godzin testów i szukania odpowiedzi; Ciekawszy efekt jest do uzyskania niż dłubanie w checkbox'ach (co oczywiście nie jest złe - wręcz przeciwnie);

Pytam właśnie o to bo jeszcze nie widziałem w żadnym programie takiego właśnie sposobu zaznaczania itemów w zwykłym komponencie TListBox;

Jeżeli znasz sposób na uzyskanie takiego zaznaczenia to proszę - podziel się wiedzą :)

0

musisz przemalowac listbox w momencie zmiany zaznaczenia jakiegos itema(ów).

0
cimak napisał(a)

musisz przemalowac listbox w momencie zmiany zaznaczenia jakiegos itema(ów).

No trzeba, trzeba - tylko w którym momencie? Kombinowałem z różnymi zdarzeniami - OnMouseDown, OnMouseUp, OnClick i OnKeyDown, OnKeyUp żeby jeszcze klawiaturę obsłużyć odpowiednio i nawet pomaga, ale nie we wszystkim; Największy problem jest z momentem zaznaczania myszką jak pisałem wcześniej; Jak wcisnę LPM i na wciśniętym zaznaczam kolejne itemy to się rysowanie wysypuje, ale jak puszczę LPM to lista się odświeża i jest już dobrze; Wszystko tyczy się właśnie samego zaznaczania na wciśniętym LPM; Niestety bardzo mało czasu mam ostatnio i ciężko znaleźć chwilkę by poszukać sposobu na rozwiązanie problemu;

TomRiddle napisał(a)

Ja to bym skopiował tą część kodu z vcl która odpowiada za TListBox, zrobił na jej podstawie nowy komponent i pozmieniałbym parę rzeczy, np. zamiast pojedynczej zmiennej przechowującej pozycję to tablica, zamiast repaint; przy każdym kliknięciu to coś innego etc.

Chyba tak trzeba będzie zrobić; Albo ręcznie obsłużyć już nie zdarzenia, a komunikaty;

0

ciezka sprawa. głupia.
wydawałoby się, ze ListBox.Selected[] jest scisle powiazane z (odSelected in State).
niestety, ListBox.Selected[] jest aktualizowany dopiero po zakonczeniu zaznaczania wiec nie wypali, jesli w trakcie zaznaczania bedziemy chcieli poznac stan reszty itemow.

0

No no, ciężka sprawa... Szkoda, że trzeba się nieźle napocić, żeby uzyskać taki prosty, a zarazem elegancki efekt; No nic, na razie w obecnym projekcie korzystam z TCheckListBox, bo jakoś ładniej wygląda w stosunku z resztą elementów GUI, ale kiedyś jak będę miał więcej czasu na eksperymenty to na pewno wrócę do tematu;

@TomRiddle, obawiam się, że moja wiedza w zakresie modyfikowania komponentów VCL jeszcze jest zbyt mała, by przerobić ten komponent na nowy z nowymi możliwościami;

Dziękuję za sugestie i poświęcony czas; Pozdrawiam :)

1 użytkowników online, w tym zalogowanych: 0, gości: 1