Umíme to s Delphi, 13. díl – textový editor

Dnes si popíšeme velmi šikovnou komponentu RichEdit a ve druhé části kapitoly ji použijeme k vytvoření jednoduchého textového editoru.
Komponenta RichEdit

Komponenta RichEdit se nachází v paletě Win32. Slouží k poměrně sofistikované práci s textem. Podíváme se na ni blíže a následně ji použijeme v reálné aplikaci – postavíme na ní textový editor.

Komponenta RichEdit obsahuje velké množství vlastností formátu textu RTF (Rich Text Format). Podrobnější informace o formátu RTF můžete nalézt třeba na www.wotsit.org.

„Odlehčenou“ verzí ovládacího prvku RichEdit je komponenta Memo, která je popsána v sedmém dílu našeho seriálu.

Vlastnosti textu (TTextAttributes)

Pomocí komponenty RichEdit můžeme mnoha způsoby pracovat s textem. Můžeme upravovat typ fontu, barvu, velikost, styl apod. Výhodou RichEditu je, že všechny tyto atributy lze nastavovat buď celému textu, nebo jen vybrané části.

Než se podíváme na konkrétní vlastnosti a metody pro práci s textem, musíme si něco říci k třídě TTextAttributes. Tato třída slouží právě k uchování informací o atributech textu a je použita v několika atributech vlastního RichEditu, ve kterých uchováváme informace o textu. Podívejme se na vlastnosti třídy TTextAttributes:

Charset - specifikuje znakovou sadu fontu. Typem jde o celé číslo v rozmezí 0…255. K dispozici je také celá řada předdefinovaných konstant:

  • ANSI_CHARSET (0) - ANSI znaky
  • DEFAULT_CHARSET (1) – font je vybrán pouze na základě specifikovaného jména a velikosti (vlastnosti Name a Size)
  • SYMBOL_CHARSET (2) - standardní symbol
  • OEM_CHARSET (255) – záleží na kódové stránce operačního systému
  • …dále jsou definovány konstanty pro jednotlivé znakové sady (např. CHINESEBIG5_CHARSET (136) – tradiční čínština, RUSSIAN_CHARSET (204), apod.
Color – barva písma. Přípusné hodnoty jsou buď přímo barvy (např. clGreen, clRed, apod.) nebo barvy definované systémem. V takovém případě záleží na nastavení barevného schématu ve Windows (např. clMenuText = barva písma v menu, clHightlightText = barva vybraného textu, apod.). Pokud máte ve svých Windows nastavenu barvu vybraného (tj. označeného) textu např. na zelenou, způsobí přiřazení barvy na clHighlightText zezelenání textu.

ConsistentAttributes – read-only vlastnost říkající, které vlastnosti TTextAttributes jsou konzistentní v celém označeném textu současného výběru v RichEditu. Jistě si dovedete představit využití – pokud bude celý označený text mít stejnou velikost písma, můžete tuto velikost zobrazit. Pokud ne, nezobrazíte nic, apod.

Height – výška (velikost) písma. Je udávána v pixelech.

Name – jméno fontu

Pitch – udává, jakou šířku mají jednotlivé znaky, lépe řečeno, mají-li všechny stejnou šířku nebo nikoliv. Přípustné hodnoty: fpDefault (hodnota záleží na fontu specifikovaném ve vlastnosti Name), fpFixed (všechny znaky mají stejnou šířku), fpVariable (znaky mají různou šířku). Poznámka: nastavení šířky písma nepřinutí Windows roztáhnout nebo smrštit znaky aktuálního fontu:-), i když by to byl jistě zajímavý pohled, ale vyberou font, který danou vlastnost splňuje. Pokud máte např. vybrán font MS Serif a nastavíte fpFixed, zobrazí se Courier.

Protected – logická hodnota indikující, je-li text reprezentovaný v TTextAttributes „protected“. Protected text nemůže být uživatelem změněn. Pokusí-li se uživatel editovat výběr, který obsahuje protected text, RichEdit generuje událost OnProtectChange. V obsluze této události můžeme povolit nebo zakázat editaci textu. Pokud jsme nevytvořili obsluhu události OnProtectChange, protected text je read-only.

Size – velikost písma. Vztah mezi hodnotou v pixelech (vlastnost Height) a hodnotou Size je následující: Size = Height * 72 / ScreenPixelsPerInch. Hodnotu ScreenPixelsPerInch lze získat z proměnné Screen.PixelsPerInch.

Style – styl textu, přípustné hodnoty: fsBold (tučné), fsItalic (kurzíva), fsUnderline (podtržené), fsStrikeout (horizontálně škrtnuté). Typ vlastnosti Style je množina, proto může obsahovat více hodnot (písmo může být zároveň podtržené a tučné, tedy [fsUnderline, fsBold]).

Tolik k vlastnostem třídy TTextAttributes. Tato třída je v RichEditu využívána pro vybraný (označený) text (tj. vlastnost SelAtributes) a ostatní text (tj. vlastnost DefAtributes). K těmto vlastnostem se ještě vrátíme v další podkapitole.

Na závěr se podívejme, jak v programu zmíněné vlastnosti nastavit. Předpokládejme, že máme na formuláři (TwndHlavni) umístěn RichEdit (reEditor). Podívejme se, jak například zjistit, je-li celý označený text tučný (nebo celý netučný):

with TwndHlavni.reEditor do
  if caBold in SelAttributes.ConsistentAttributes then
    ShowMessage(`Celý text je tučný nebo netučný`)
  else
    ShowMessage(`Část textu je tučná, část ne`);

Vlastnosti RichEditu

K práci s textem slouží především vlastnosti, které shrnuje následující tabulka. Upozorňuji, že většina jich je runtime, proto je nehledejte v Object Inspectoru.

Vlastnost Význam
DefAttributes je typu TTextAttributes (viz předchozí podkapitola) a popisuje „normální“, tedy neoznačený text. V podstatě říká, jak bude vypadat nově přidávaný text.
HideScrollBars říká, mají-li se skrýt posuvníky, nejsou-li potřeba (celý text se na stránku vejde). Nastavení této vlastnosti nemá žádný význam, pokud je vlastnost ScrollBars (celého RichEditu) nastavena na ssNone.
HideSelection říká, bude-li vizuální indikace označení textu viditelná i po ztrátě zaměření RichEditu (jednoduše: bude-li text stále vidět jako označený, když se přepnete z RichEditu jinam v rámci aplikace).
Lines klíčová vlastnost, obsahuje vlastní text. Je typu TStrings.
Paragraph specifikuje formát odstavce textu, podrobněji viz další podkapitola
PlainText velmi důležitá vlastnost, kladná hodnota (True) říká, že soubor se bude ukládat jako čistý text <.TXT), kdežto záporná hodnota (false) znamená ukládání ve formátu RTF
SelAttributes je typu TTextAttributes (viz předchozí podkapitola) a popisuje označený text.
SelLength délka (tj. počet znaků) označeného textu
SelStart pozice začátku označeného textu (ve znacích od začátku dokumentu, 0 znamená první znak)
SelText označený text. Tato vlastnost je typu string (nikoliv TStrings)
WordWrap říká, budou-li zalamovány řádky, které přesahují viditelnou oblast obrazovky

Než se podíváme na vlastnosti odstavce, povíme si ještě o několika metodách komponenty RichEdit, které jsou velmi praktické:

Metoda Význam
FindText Mocná vlastnost, vyhledává text v RichEditu. Parametry: FindText (co: string; odkud, délka: Integer, možnosti: TSearchTypes). Tato metoda je použita v příkladu níže.
Print Vytiskne obsah RichEditu
ClearSelection Vymaže označený text
CopyToClipboard Zkopíruje označený text do schránky
CutToClipboard Vystřihne označený text do schránky, tj. text z dokumentu vyjme
PasteFromClipboard Vloží na pozici kurzoru text ze schránky. Je-li označen jiný text, nahradí se.
SelectAll Označí celý dokument

Odstavec (Paragraph)

Jednou z vlastností komponenty RichEdit je odstavec (Paragraph). Paragraph je runtime vlastností, která specifikuje formátovací informaci aktuálního odstavce. Formátovací informací rozumíme zarovnávání, odrážky, číslování a tabulátory. Aktuálním odstavcem je myšlen odstavec obsahující označený text. Pokud není označen žádný text, je aktuálním odstavcem ten, na (ve) kterém je kurzor. Paragraph je typu TParaAttributes.

Poznámka pro pokročilé uživatele:

Paragraph je read-only vlastnost, protože objekt TCustomRichEdit má pouze jeden objekt TParaAttributes, který nemůže být měněn. Atributy aktuálního odstavce ovšem mohou být měněny, a to nastavováním vlastností objektu TParaAttributes. Mohou být nastavovány jedna po druhé nebo najednou podle existujícího objektu TParaAttributes zavoláním Paragraph.Assign.

Podívejme se tedy na vlastnosti odstavce.

Vlastnost Význam
Alignment Specifikuje, jak má být text odstavce zarovnán. Přípustné hodnoty: taLeftJustify (vlevo), taCenter (na střed), taRightJustify (vpravo).
FirstIndent Říká, jak moc bude první řádka odstavce „odražená“ od levého okraje, udává se v pixelech.
LeftIndent Říká, jak moc budou ostatní řádky odstavce „odraženy“ od levého okraje. Tato vlastnost je použitelná např. k „vypíchnutí“ některého důležitého odstavce, který má jiné postavení v textu, apod. LeftIndent, stejně jako předchozí FirstIndent, nesouvisí s odrážkami (tedy puntíky).
Numbering Specifikuje odrážku aktuálního odstavce (tedy onen puntík). Je-li nastavena vlastnost LeftIndent, je odrážka posunuta o uvedenou vzdálenost. Přípustné hodnoty: nsNone, nsBullet.
RightIndent Analogie LeftIndent. Je efektní zvláště při nastavení Alignment = taRightJustify.
Tab Určuje počet pixelů, po kterých se bude „poskakovat“ tabulátorem. Tab je indexovaná vlastnost. Pozice prvního „tab stopu“ je tedy Tab[0], dalšího Tab[1], atd.
TabCount Počet definovaných zastávek tabulátoru („tab stopů“) v tomto odstavci.

Podívejme se na příklad procedury, která definuje pozici zarážek tabulátoru:

procedure NastavTabStops (RichEdit: TRichEdit; TabStops: array of Integer);
var
  i: Integer;  // indexy oznacujeme zpravidla malymi pismeny

begin
  with RichEdit.Paragraph do begin
    TabCount := High(TabStops) + 1;  // nejvyssi index + 1
    for i := 0 to TabCount – 1 do
      Tab[i] := TabStops[i];
  end;
end;

Nyní nadefinujeme zarážky tabulátoru:

NastavTabStops(reEditor, [25, 50, 80, 100]);

Vyhledávání v textu

Tolik tedy obecně k RichEditu. Nyní již jsme schopni vytvořit onen kýžený textový editor, ovšem nejprve vyřešíme restík, který nám zbyl z předchozího dílu. Tam jsme si ukázali dialogy pro vyhledávání a nahrazování v textu – FindDialog, resp. ReplaceDialog. Řekli jsme si, že je ovšem nutné ručně zpracovat vlastní vyhledávání. Na to se tedy nyní vrhneme.

Uvedu možnou obsluhu události pro vyvolání (otevření) FindDialogu (např. po stisku tlačítka, vybrání příkazu v menu apod.). Její součástí bude automatické vyplnění políčka s vyhledávaným textem v případě, že uživatel nějaký text označil.

procedure TwndHlavni.btnNajdiClick(Sender: TObject);
begin
  dlgFind.FindText := reEditor.SelText;
  dlgFind.Execute;
end;

Následuje kód ošetřující událost OnFind dialogu FindDialog:

procedure TwndHlavni.dlgFindFind(Sender: TObject);
var
  Pozice: Longint;

begin
  with reEditor do begin
    Pozice := Pos(dlgFind.FindText, Lines.Text);
    if Pozice > 0 then begin
      SelStart := Pozice - 1;
      SelLength := Length(dlgFind.FindText);
    end
    else
      MessageDlg(`Nenalezeno!`, mtError, [mbOk], 0);

    SetFocus;  // jako aktivni nastav reEditor
    dlgFind.CloseDialog;
  end;
end;

Poznámka: tento kód je použitelný k hledání obecně. Pokud ale vyhledáváme konkrétně v RichEditu (což se právě chystáme provádět), je lepší použít vlastnost RichEditu FindText a implementovat vyhledávání takto:

procedure TwndHlavni.dlgFindFind(Sender: TObject);
var
  Pozice: Longint;

begin
  with reEditor do begin
    Pozice := reEditor.FindText(dlgFind.FindText, 0, Length(text), []);
    if Pozice > 0 then begin
      SelStart := Pozice;
      SelLength := Length(dlgFind.FindText);
    end
    else
      MessageDlg(`Nenalezeno!`, mtError, [mbOk], 0);

    dlgFind.CloseDialog;
    SetFocus;
  end;
end;

Vytváříme textový editor

Nyní již nám nechybí vůbec nic k tomu, abychom naprogramovali textový editor. Protože to určitě zvládnete sami, budu psát jen body a podrobněji se zastavím jen u „zapeklitějších“ situací.

Vytvoříme nový projekt. Na formulář umístíme následující komponenty (v závorce uvedu, jak je nazveme): RichEdit (reEditor), MainMenu (mnHlavni), PopupMenu (mnPopup). Dále na formulář umístíme dialogy: OpenDialog (dlgOpen), SaveDialog (dlgSave), FontDialog (dlgFont), ColorDialog (dlgColor), PrintDialog (dlgPrint), PrinterSetupDialog (dlgSetup), FindDialog (dlgFind).

Otevřeme si Menu Designer u mnHlavni a vytvoříme hlavní nabídku programu. V té budou následující položky:

  • Soubor - s podpoložkami Nový, Otevřít, Ulož, Ulož jako, Nastavení tisku, Tisk, Konec
  • Úpravy – s podpoložkami Kopíruj, Vystřihni, Vlož, Vybrat vše, Najít
  • Formát – s podpoložkami Písmo, Barva, Zalamování řádků
  • Nápověda – s podpoložkami O programu a Nápověda.
Poznámka: mějte na paměti, že správně vytvořené menu (viz 9. díl našeho seriálu) by mělo obsahovat možnost zkrácené volby (tj. používání ampersandu & před příslušným písmenem textu položky) a možnost používat klávesové zkratky (např. Ctrl+C by mělo zkopírovat označený text apod.).

V návrhu aplikace je ještě vhodné nastavit „Default“ hodnotu titulku okna. Aby bylo možno kód plně opět využít:-), použijeme konstantu JM_PRG, která bude obsahovat název programu, tedy v našem případě „Textový editor“. Další konstanta (NO_NAME) označuje název souboru „Bez názvu.txt“. Konstanty budou definovány „nahoře“ v modulu, tedy v sekci definic, buď v části interface nebo implementation.

const
  JM_PRG  = `dfsd`;
  NO_NAME = `Bez názvu.txt`;

wndHlOkno.Caption := NO_NAME + JM_PRG;

Nyní přijde důležitý okamžik. Budeme postupně ošetřovat jednotlivé položky menu.

Soubor – Nový: program by se měl zeptat, zda uložit změněný soubor (pokud byl změněný), a pak vymazat celý obsah RichEditu. K tomu účelu je možné např. vytvořit funkci UlozZmeny, která zjistí, zda byl soubor změněn, a podle uživatelova přání jej uloží. Použitelná je vlastnost RichEditu Modified a událost RichEditu OnChange. Použitá procedura UlozSoubor bude definována níže. Budeme udržovat privátní atribut JmenoSouboru, ve kterém bude uloženo jméno právě zpracovávaného souboru. V souladu s poznatky o objektově orientované architektuře víme, že je nutné definovat metodu, která bude toto jméno souboru nastavovat. Ukázka kódu funkce pro nastavení jména:

procedure TwndHlavni.NastavJmenoSouboru(const Jmeno: String);
begin
  JmenoSouboru := Jmeno;
  wndHlavni.Caption := ExtractFileName(JmenoSouboru) + ` - ` + JM_PRG;
end;

Poznámka: při každém vkládání nového jména souboru budeme muset volat funkci NastavJmenoSouboru. To je sice poměrně běžný postup, ale není úplně hifi:-), to jaksi všichni cítíme. Ideálním řešením by bylo použití tzv. property. Tato problematika bude popsána v druhém díle zaměřeném na objektové koncepty.

Kód funkcí pro test změny textu uložení změn:

function TwndHlavni.UlozZmeny: TModalResult;
begin
  if reEditor.Modified then begin
    Result:= MessageDlg(`Soubor byl změněn, chcete jej uložit?`, mtConfirmation, mbYesNoCancel, 0);
    if Result = mrYes then
      UlozSoubor;
  end
  else
    Result := mrOK;
end;

function TwndHlavni.UlozSoubor;
begin
  reEditor.Lines.SaveToFile(JmenoSouboru);
  reEditor.Modified := False;
end;

Kód vlastní položky menu:

procedure TwndHlavni.NovyClick(Sender: TObject);
begin
  if (UlozZmeny <> mrCancel) then begin
    NastavJmenoSouboru(NO_NAME);

    reEditor.Lines.Clear;
    reEditor.Modified := False;
  end;
end;

Soubor – Otevři: program by měl nejprve otestovat (stejně jako v předchozím případě), zda je nutné soubor uložit. Pak by měl otevřít dialog OpenDialog. Protože RichEdit jako takový umožňuje práci se soubory TXT a RTF, měl by tento dialog být nastaven pro práci právě se zmíněnými soubory. Ukázka kódu:

procedure TwndHlavni.OtevriClick(Sender: TObject);
begin
  if (UlozZmeny <> mrCancel) then
    if dlgOpen.Execute then begin
      NastavJmenoSouboru(dlgOpen.FileName);
      reEditor.Lines.LoadFromFile(JmenoSouboru);
      reEditor.Modified := False;
      reEditor.ReadOnly := ofReadOnly in dlgOpen.Options;
    end;
end;

Soubor – Ulož: soubor bude (bez ptaní) uložen pod svým aktuálním jménem. Jedinou výjimkou bude situace, kdy se soubor bude jmenovat „Bez názvu.txt“. V takovém případě se otevře dialog Uložit jako.

procedure TwndHlavni.UlozClick(Sender: TObject);
begin
  if JmenoSouboru = NO_NAME then
    UlozJakoClick(Sender)
  else
    UlozSoubor;
end;

Soubor – Ulož jako: bude otevřen dialog SaveFile, ve kterém bude uživatel moci vybrat jméno, pod kterým se bude soubor ukládat.

procedure TwndHlavni.UlozJakoClick(Sender: TObject);
begin
  if dlgSave.Execute then begin
    NastavJmenoSouboru(dlgSave.FileName);
    UlozSoubor;
  end;
end;

Další příkazy musíme z důvodu rozsahu článku probrat již jen povrchně.

Soubor - Nastavení tisku: vyvoláme dialog PrintSetupDialog

Soubor - Tisk: vyvoláme dialog PrintDialog. Ukážeme si kód této procedury:

procedure TwndHlavni.TiskClick(Sender: TObject);
begin
  if dlgPrint.Execute then
    reEditor.Print(JmenoSouboru);
end;

Soubor - Konec: nejprve bychom měli opět otestovat, zda je soubor potřeba uložit. Pak ukončíme aplikaci. Není zrovna vhodné provádět další explicitní dotazy typu „Chcete opravdu skončit?“, protože uživatel bude často potřebovat editor opakovaně vypínat a znovu spouštět. Tato situace může nastat třeba při editaci textu, jehož originál je umístěn na vzdáleném stroji. Pak je totiž potřeba editor zavřít, aby byla lokální kopie souboru přenesena zpět (typicky Windows Commander).

Úpravy - Kopíruj: na zkopírování označeného textu do schránky existuje metoda komponenty RichEdit (lépe řečeno zděděná metoda z TCustomEdit).

procedure TwndHlavni.CopyClick(Sender: TObject);
begin
  wndHlavni.reEditor.CopyToClipboard;
end;

Úpravy – Vyřízni, Vlož: k těmto dvěma činnostem existují analogické metody komponenty RichEdit jako pro kopírování do schránky. Popsány jsou v tabulce v předchozí podkapitole.

Úpravy – Vybrat vše: opět existuje metoda, konkrétně SelectAll.

Úpravy – Najít: kód, který uvedeme do obsluhy. Události vybrání této položky v menu jsou uvedeny v předchozí podkapitole, stejně jako kód, který je nutno napsat do obsluhy události OnFind dialogu FindDialog.

Formát – Písmo: nejprve zjistíme, zda je označena nějaká část textu (a zda se tedy bude nastavování týkat atributu SelAttributes nebo DefAttributes). Pak zavoláme dialog FontDialog:

procedure TwndHlavni.PismoClick(Sender: TObject);
begin
  with reEditor do begin
    if SelLength > 0 then begin
      dlgFont.Font.Assign(SelAttributes);
      if dlgFont.Execute then
        SelAttributes.Assign(dlgFont.Font);
    end
    else begin
      dlgFont.Font.Assign(DefAttributes);
      if dlgFont.Execute then
        DefAttributes.Assign(dlgFont.Font);
    end;
  end;
end;

Formát – Barva: provedeme analogicky jako font

Formát – Zalamování řádků: použjeme vlastnosti RichEditu WordWrap. Praktické je u této položky v menu zobrazovat zatržení říkající, zda je tato volba právě aktivována či nikoliv.

Nápověda – O programu: vytvoříme volbou v menu File – New Form nový formulář. Na jeho designu se můžeme klidně vyřádit; pokud nechceme zůstat u „střízlivého“ vzhledu, zkusíme si třeba zařadit na formulář nějaký posuvný text (námět na domácí úkol – víte, jak na to?).

Nápověda – Nápověda: buďto vytvoříme klasickou nápovědu (soubor *.HLP, což je vcelku „pakárna“ a kromě toho to bude obsahem samostatného dílu našeho seriálu), nebo budeme líní (a u takovéto aplikace by to asi až tolik nevadilo) a nápovědu vytvoříme jako nový formulář s komponentou Memo (nebo RichEdit:-)), do které napíšeme text nápovědy, tedy stručné shrnutí voleb a možností programu.

Pryč s Poznámkovým blokem

Jednoduchý editor máme hotov. Můžete ze svého systému vymazat Poznámkový blok a používat místo něho váš vlastní výtvor – určitě vás to při každém použití potěší. Samozřejmě, nabízelo by se mnoho vylepšení. Iniciativě se meze nekladou. Můžete vylepšit editor o stavový řádek (na kterém by se třeba zobrazovaly údaje o aktuálním souboru, číslo řádky apod). Dále by se daly implementovat statistické údaje, např. počet řádků, znaků; dále aktuální doba psaní apod. Vzhled by mohly vylepšit ikonky (pro otevření a uzavření souboru) – na vylepšování formulářů se podíváme právě v příštím díle. Na závěr dodávám, že podobný příklad je k nalezení v podadresáři DEMOS\RICHEDIT adresáře, ve kterém máte nainstalováno Delphi. Je ovšem samozřejmě v angličtině.

A ještě jedna poznámka na závěr: kdo mi jako první pošle vytvořený vlastní zdroják příkladu z tohoto dílu seriálu (s dobrou štábní kulturou:-)), získá čestný titul „Živě’s Best Delphi Programmer“ a nehynoucí slávu plynoucí ze zveřejnění svého jména v příštím díle seriálu…:-)

Diskuze (10) Další článek: Windows 98 dostanou podporu USB 2.0

Témata článku: , , , , , , , , , , , , , , , , , , , , , , , , ,