Standardowe kontrolki, Ctrl+BkSp i usuwanie poprzedniego wyrazu

0

Potrzebuję zaimplementować w standardowych kontrolkach (głównie TMemo i TEdit/TLabeledEdit) usuwanie poprzedniego wyrazu za pomocą kombinacji Ctrl + BkSp tak, jak ma to miejsce np. w edytorze kodu w Delphi 7;

Kombinowałem już ze zdarzeniami OnKeyDown, OnKeyUp i OnKeyPress zarówno kontrolki jak i formularza, z włączonym i wyłączonym KeyPreview formularza rodzica, niestety bez powodzenia; Bez względu na powyższe ustawienia w danej kontrolce i tak zostaje wpisany kwadracik, mimo zerowania argumentu Key we wszystkich zdarzeniach; Próbowałem także wykorzystać prywatną zmienną typu Boolean - ustawiałem na True w zdarzeniu OnKeyDown jeśli ssCtrl został wciśnięty (ssCtrl in Shift), oraz na False w OnKeyUp gdy został puszczony (ssCtrl in Shift) po to, bym mógł sprawdzić stan tego specialnego klawisza w zdarzeniu OnKeyPress, jednak także bez powodzenia; Niestety zawsze, gdy wykombinuję Ctrl + BkSp do kontrolki zostaje wpisany kwadracik;

Pośrednio można to rozwiązać przez dodanie do menu głównego nową pozycję typu Usuń poprzedni wyraz i ustawiając jej HotKey na Ctrl+BkSp, jednak wolałbym tego nie robić (choć w ostateczności przymknę okno); W każdym formularzu, gdzie istnieją kontrolki klas TMemo czy TEdit/TLabeledEdit zarówno w menu głownych jak i kontekstowych jest odpowiednia sekcja Edycja i mógłbym tam dodać taką pozycję, jednak wolałbym te menu pozostawić bez zmian, gdyż ich rozbudowa zahaczałaby pod modyfikację własnych paczek ikon i większe przeróbki; To oczywiście nie jest żaden problem, ale takie modyfikacje pochłaniają czas, który wolałbym przeznaczyć na rozbudowę innych elementów programu;

W sieci znalazłem mnóstwo artykułów na temat blokowania klawiszy VK_BACK czy VK_DELETE (co już stosowałem dawno temu), ale w połączeniu z Ctrl nie sprawdzą się;

To ile znaków i dokąd odbędzie się usuwanie jeszcze nie określiłem dokładnie, na razie testowałem z usuwaniem ciągu znaków do znaku ' ' (spacji), ale nie udało się;

Czy ktokolwiek wie w jaki sposób można osiągnąć taki efekt? Jak usunąć poprzedni wyraz do jakiegokolwiek znaku? Będę bardzo wdzięczny za wskazówki;

3

Tak na szybko wymyśliłem coś takiego:

procedure TForm1.OnMsg(var Msg: TMsg; var Handled: Boolean);
var
  ctrl: TWinControl;
  len, i: Integer;
  buff: string;
begin
  if Msg.message = WM_KEYDOWN then
  begin
    //czy ctrl + backspace wcisniety?
    if (Msg.wParam = VK_BACK) and (GetKeyState(VK_CONTROL) and 128 = 128) then
    begin
      ctrl:= Screen.ActiveControl; //aktywna kontrolka
      //czy o taka nam chodzi?
      if Assigned(ctrl) and ((ctrl is TEdit) or (ctrl is TMemo) or (ctrl is TLabeledEdit)) then
      begin
        len:= GetWindowTextLength(Msg.hwnd);  //pobierz dlugosc tekstu
        SetLength(buff, len);  //ustaw wielkosc buffera
        GetWindowText(Msg.hwnd, PChar(buff), len + 1); //pobierz tekst
        //usun spacje i znaki konca wiersza na koncu tekstu
        while (Length(buff) > 0) and ((buff[Length(buff)] = #32) or
          (buff[Length(buff)] = #13) or (buff[Length(buff)] = #10)) do
          SetLength(buff, Length(buff) - 1);
        for i:=Length(buff) downto 1 do //usuwaj znaki do napotkania spacji lub konca wiersza
        begin
          if (buff[i] = #32) or (buff[i] = #13) or (buff[i] = #10) then
            break;
          SetLength(buff, Length(buff) - 1);
        end;
        //usun spacje i znaki konca wiersza na koncu tekstu
        while (Length(buff) > 0) and ((buff[Length(buff)] = #32) or
          (buff[Length(buff)] = #13) or (buff[Length(buff)] = #10)) do
          SetLength(buff, Length(buff) - 1);
        SetWindowText(Msg.hwnd, PChar(buff)); //zmien tekst
        SendMessage(Msg.hwnd, EM_SETSEL, Length(buff), Length(buff)); //ustaw kursor na koncu tekstu
        Handled:= True; //komunikat zostal obsluzony
      end;
    end;
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  Application.OnMessage:= OnMsg;
end;

Użyłem funkcji WinApi aby kod był jak najbardziej uniwersalny TWinControl nie ma właściwości Text i trzeba by sprawdzać za każdym razem (lub pisać osobny kod dla każdego rodzaju kontrolki) i odpowiednio rzutować a tu masz wszystkie te kontrolki (działało by też z TRichEdit) za jednym zamachem.

0

Nie ma to jak powrót do WinAPI :]

Kod dopiero co przetestowałem i fakt, komunikat zostaje poprawnie obsłużony, jednak nie do końca chodziło mi o taki efekt; W Twoim kodzie @kAzek usuwany jest zawsze ostatni wyraz ciągu bez względu na to gdzie znajduje się kursor w TMemo, ale nic nie stoi na przeszkodzie aby go troszkę przerobić - to nic trudnego; Wystarczyło jedynie dowiedzieć się, że trzeba się podpiąć pod zdarzenie OnMessage i po obsłudze danego komunikatu ustawić Handled, by po raz kolejny go nie przetwarzano; Dziś się tym pobawię i jak skończę to udostępnię gotowy kod do testowania;

Dziękuję jeszcze raz, pozdrawiam;


Edit

Podaję kod, który realizuje zadanie usuwania wyrazu z lewej strony kursora w kontrolkach klas TMemo i TEdit:

procedure TMainForm.OnMessage(var AMsg: TMsg; var AHandled: Boolean);
const
  STOP_CHARS_ARR = [#10, #13, #32, #33, #40, #41, #44, #45, #46, 
                    #47, #63, #91, #92, #93, #123, #124, #125];
var
  ctrlActive: TWinControl;
  iCaretPos, iLength: Integer;
  sLeft, sRight: String;
begin
  if AMsg.message = WM_KEYDOWN then
    if (AMsg.wParam = VK_BACK) and (GetKeyState(VK_CONTROL) and 128 = 128) then
      begin
        ctrlActive := Screen.ActiveControl;

        if Assigned(ctrlActive) and ((ctrlActive is TMemo) or (ctrlActive is TEdit)) then
          begin
            iLength := GetWindowTextLength(AMsg.hWnd);
            iCaretPos := LoWord(SendMessage(AMsg.hWnd, EM_GETSEL, 0, 0));

            SetLength(sLeft, iLength);
            GetWindowText(AMsg.hWnd, PChar(sLeft), Succ(iLength));
            sRight := Copy(sLeft, Succ(iCaretPos), iLength - iCaretPos);
            SetLength(sLeft, iCaretPos);

            while (Length(sLeft) > 0) and (sLeft[Length(sLeft)] in STOP_CHARS_ARR) do
              SetLength(sLeft, Pred(Length(sLeft)));

            while (Length(sLeft) > 0) and not (sLeft[Length(sLeft)] in STOP_CHARS_ARR) do
              SetLength(sLeft, Pred(Length(sLeft)));

            while (Length(sLeft) > 0) and (sLeft[Length(sLeft)] in [#10, #13, #32]) do
              SetLength(sLeft, Pred(Length(sLeft)));

            SetWindowText(AMsg.hWnd, PChar(sLeft + sRight));
            SendMessage(AMsg.hWnd, EM_SETSEL, Length(sLeft), Length(sLeft));
            AHandled := True;
          end;
      end;
end;

Mam jeszcze jedno pytanie odnośnie ww. kodu - jak wiadomo klasa TMemo ma właściwość Modified, której wartość zmienia się na True, jeśli zmieni się zawartość kontrolki; Jeśli ww. kod zostanie wykonany to nie dość, że nie zmienia mi wartości tej właściwości, to jeszcze kasuje bufor cofania (CanUndo na False) i już nie można cofnąć; Czy jest jakiś sposób na ochronienie CanUndo i na zmianę stanu właściwości Modified po wykonaniu powyższego kodu?

1
furious programming napisał(a):

Mam jeszcze jedno pytanie odnośnie ww. kodu - jak wiadomo klasa TMemo ma właściwość Modified, której wartość zmienia się na True, jeśli zmieni się zawartość kontrolki; Jeśli ww. kod zostanie wykonany to nie dość, że nie zmienia mi wartości tej właściwości, to jeszcze kasuje bufor cofania (CanUndo na False) i już nie można cofnąć; Czy jest jakiś sposób na ochronienie CanUndo i na zmianę stanu właściwości Modified po wykonaniu powyższego kodu?

Z tym może być problem bo:
The system automatically resets the undo flag whenever an edit control receives an EM_SETHANDLE or WM_SETTEXT message. a ten komunikat jest wysyłany przy zmianie tekstu przez SetWindowText i SetDlgItemText a więc d... blada i żadne EM_SETMODIFY tu nie pomoże bo bufor i tak jest pusty.

Co mogę zaproponować to "małe oszustwo" jak wiadomo kontrolki typu EDIT (memo to edit tylko z ES_MULTILINE) obsługują komunikaty EM_SETSEL i EM_REPLACESEL a ten drugi pozwala na ustawienie flagi CanUndo w wParam więc zaznaczyć tekst który ma być usunięty przy ctrl + backspace i zmienić na pusty wtedy teoretycznie (bo nie sprawdzałem) powinno wszystko działać jak należy. Myślę że takie programowe zaznaczenie i zmiana zaznaczonego tekstu powinno być na tyle szybkie że nie będzie widoczne dla użyszkodnika więc raczej nic nie stoi na przeszkodzie aby wykorzystać takie rozwiązanie (o ile zadziała bo nie sprawdzałem).

EDIT//
Miałem chwilę czasu i sprawdziłem z tym EM_SETSEL (zaznaczenie tekstu który trzeba wyciąć) i EM_REPLACESEL (zmiana go na pusty czyli wycinanie) działa ale po wykonaniu Cofnij tekst jest zaznaczony więc trochę nie ładnie... ale na razie nie mam innego pomysłu.

0

Podłubałem troszkę przy EM_SETSEL i EM_REPLACESEL i w końcu działa; Poczytałem o tej drugiej fladze i aby dać możliwość cofnięcia trzeba wParam ustawić na True, np. Integer(True) lub po prostu 1;

Działający kod:

procedure TMainForm.OnMessage(var AMsg: TMsg; var AHandled: Boolean);
const
  STOP_CHARS_ARR = [#10, #13, #32, #33, #40, #41, #44, #45, #46,
                    #47, #63, #91, #92, #93, #123, #124, #125];
var
  ctrlActive: TWinControl;
  iTextLen, iCaretPos, iSelStart: Integer;
  sLeft: String;
begin
  if AMsg.message = WM_KEYDOWN then
    if (AMsg.wParam = VK_BACK) and (GetKeyState(VK_CONTROL) and 128 = 128) then
      begin
        ctrlActive := Screen.ActiveControl;

        if Assigned(ctrlActive) and ((ctrlActive is TMemo) or (ctrlActive is TEdit)) then
          begin
            AHandled := True;

            if (ctrlActive is TEdit) and TEdit(ctrlActive).ReadOnly then
              Exit;

            iTextLen := GetWindowTextLength(AMsg.hWnd);
            iCaretPos := LoWord(SendMessage(AMsg.hWnd, EM_GETSEL, 0, 0));
            SetLength(sLeft, iCaretPos);
            GetWindowText(AMsg.hWnd, PChar(sLeft), iCaretPos);
            iSelStart := iCaretPos;

            while (iSelStart > 0) and (sLeft[iSelStart] in STOP_CHARS_ARR) do
              Dec(iSelStart);

            while (iSelStart > 0) and (not (sLeft[iSelStart] in STOP_CHARS_ARR)) do
              Dec(iSelStart);

            while (iSelStart > 0) and (sLeft[iSelStart] in [#10, #13, #32]) do
              Dec(iSelStart);

            if iSelStart < iCaretPos then
              begin
                SendMessage(AMsg.hWnd, EM_SETSEL, iSelStart, iCaretPos);
                SendMessage(AMsg.hWnd, EM_REPLACESEL, Integer(True), Integer(PChar('')));

                if (GetWindowTextLength(AMsg.hWnd) <> iTextLen) and (ctrlActive.Name = 'memNote') then
                  begin
                    UpdateToolbar();
                    UpdateNoteControls();
                  end;
              end;
          end;
      end;
end;

Procedury UpdateToolbar i UpdateNoteControls odświeżają przyciski na pasku narzędzi i inne kontrolki powiązane z polem opisu w głównym formularzu; Warunek if iSelStart < iCaretPos then jest konieczny, bo jeśli nic nie będzie usuwane to komunikat nie zostanie wysłany i nie zmieni mi właściwości Modified pola na False, a o to mi chodzi;

Dziękuję @kAzek za pomoc, pozdrawiam :]

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