8. Dvojrozmerné polia, obrázky


Posledná zmena: 15.10.2002

Banner Text 15.10.2002

    čo sme sa doteraz naučili

    • jednorozmerné polia prvkov rovnakého typu, napr. jednoduché typy alebo korytnačky
    • štruktúrovaný typ záznam

    čo sa budeme dnes učiť

    • ukážeme, že prvkami poľa môžu byť reťazce ale aj iné polia
    • ukážeme, ako sa pracuje s obrázkami v grafickej ploche - budeme pracovať po jednotlivých pixeloch (farebné body obrázka)
    • uvidíme nové vlastnosti komponentov
      • Memo - práca s riadkami pomocou Strings[]
      • Image - práca s farebnými bodkami pomocou Pixels
      • Button - text na tlačidle Title
      • a tiež sa zoznámime s novým komponentom Edit
    • uvidíme niektoré jednoduché udalosti (onMouseMove, onKeyPress)

Pole znakových reťazcov

  • údajová štruktúra pole môže mať prvky ľubovoľného typu, teda aj znakový reťazec
  • s takýmito premennými, ktoré sú prvkami poľa, sa pracuje rovnako ako s jednoduchými premennými typu reťazec

program ilustruje použitie poľa znakových reťazcov:

var
  t:TextFile;
  p:array [1..1000] of string;
  i,j,n:integer;
begin
  AssignFile(t,'unit1.pas'); Reset(t);
  n:=0;
  while not eof(t) and (n<high(p)) do begin
    inc(n); readln(t,p[n]);
  end;
  CloseFile(t);
  for i:=1 to n do
    for j:=1 to length(p[i]) do
      if (j=1) or (p[i][j-1]=' ') then p[i][j]:=upcase(p[i][j]);
  Memo1.Clear;
  for i:=1 to n do
    Memo1.Lines.Add(p[i]);
end;
  •  zápis p[i][j] označuje j-ty znak i-teho reťazca - môžeme to zapísať skrátene: p[i,j]

časť programu filtruje medzery na začiatku a konci každého reťazca:

  ...
  for i:=1 to n do begin
    j:=1; while (j<=Length(p[i])) and (p[i,j]=' ') do inc(j);
    delete(p[i],1,j-1);
    j:=Length(p[i]); while (j>=1) and (p[i,j]=' ') do dec(j);
    SetLength(p[i],j);
  end;
  ...
  • zrejme toto isté by sa dalo jednoduchšie zrealizovať pomocou štandardnej funkcie Trim

Textová plocha

  • Ešte sa vrátime ku komponentu textová plocha - TMemo. Už poznáme metódy:
    • Memo1.Lines.Clear; - vyčistí všetky riadky
    • Memo1.Lines.Add('nejaký text'); - pridá ďalší riadok s daným textom na koniec
    • Memo1.Lines.LoadFromFile('meno_súboru'); - obsah plochy nahradí obsahom zadaného súboru
  • Na riadky (Lines) textovej plochy sa môžeme pozerať ako na jednorozmerné pole znakových reťazcov Strings. Metóda Clear toto pole inicializuje, t.j. zruší všetky prvky poľa a prvý prvok (s indexom 0) vyprázdni. Metóda Add pridá na koniec tohoto poľa nový reťazec. Momentálny počet riadkov zistíme pomocou Memo1.Lines.Count. Pomocou Strings môžeme modifikovať konkrétne riadky textovej plochy, napr.
    • Memo1.Lines.Strings[0]:='mmmmmm';

zmeníme každý riadok textovej plochy:

procedure TForm1.Button1Click(Sender: TObject);
var
  i:integer;
begin
  for i:=0 to Memo1.Lines.Count-1 do
    Memo1.Lines.Strings[i]:='<'+Memo1.Lines.Strings[i]+'>';
end;
  • logická stavová premenná Memo1.ReadOnly slúži na to, aby sme používateľovi mohli povoliť/zakázať zapisovať do textovej plochy, napr. Memo1.ReadOnly:=false; zakáže používateľovi modifikovať plochu pomocou klávesnice a myši (príkazmi z programu je to stále dovolené)

Viacrozmerné polia

Zovšeobecníme polia: prvkami poľa môže byť opäť pole -- ak je prvkom jednorozmerné pole, tak výsledný typ je dvojrozmerné pole. Napr. a[4][2] označuje druhý prvok v štvrtom poli - väčšinou si dvojrozmerné pole predstavujeme ako tabuľku, v ktorej každý prvok leží v nejakom riadku a nejakom stĺpci, napr. a[4][2] môže označovať prvok v 4-tom riadku a v 2-om stĺpci - skrátene to môžeme zapísať a[4,2]

  • Príklad: Je dané dvojrozmerné pole znakov, ktoré je už zaplnené. Napíšte program, ktorý presunie prvý riadok do druhého, druhý do tretieho, atď., až posledný do prvého (motivácia: rolovanie obrázka ako vo svetelných novinách).

Najprv nie pekné riešenie:

var
  a:array[1..10] of array[1..5] of char;
  r:array[1..5] of char;
  i,j:integer;
begin
  ... // zaplnenie poľa
  for i:=1 to 5 do r[i]:=a[10,i];
  for i:=9 downto 1 do
    for j:=1 to 5 do
      a[i+1,j]:=a[i,j];
  for i:=1 to 5 do a[1,i]:=r[i];
  ...

a teraz krajšie

type
  riadok=array[1..5] of char;
  pole=array[1..10] of riadok;
var
  a:pole;
  r:riadok;
  i:integer;
begin
  ...
  r:=a[10];
  for i:=9 downto 1 do a[i+1]:=a[i];
  a[1]:=r;
  ...
  • využili sme to, že typ každého riadka poľa a je rovnaký ako premenná r
  • nasledujúci príklad ukazuje spôsob prezerania prvkov dvojrozmerného poľa (zisťujeme, či je matica NxN symetrická)

nie najlepší spôsob riešenia:

const
  N=10;
type
  pole=array[1..N,1..N] of integer;
var
  a:pole;
  i,j:integer;
  ok:boolean;
begin
  // ... inicializácia poľa
  ok:=true;
  for i:=2 to N do
    for j:=1 to i-1 do
      if a[i,j]<>a[j,i] then begin
        ok:=false;
        break;
      end;
von:
  if ok then     // ... je symetrická
    ...
end;
  • toto riešenie sa snaží použiť príkaz break, ktorý spôsobí vyskočenie z najvnútornejšieho cyklu - toto je nedostatočné, lebo aj tak sa budú kontrolovať ešte všetky nasledujúce riadky poľa
  • ďalšie riešenie je zlé - používa príkaz goto - tento príkaz je pre nás absolútne zakázaný

veľmi zlý spôsob riešenia:

  ...
  ok:=true;
  for i:=2 to N do
    for j:=1 to i-1 do
      if a[i,j]<>a[j,i] then begin
        ok:=false;
        goto von;     // !!! je to absolútne zakázané !!!
      end;
    ...

správne riešenie:

  ...
  ok:=true; i:=2;
  while ok and (i<=N) do begin
    j:=1;
    while ok and (j<i) do begin
      ok:=a[i,j]=a[j,i];
      inc(j)
    end;
    inc(i);
  end;
  ...

správne riešenie s využitím exit vo funkcii:

function test(const a:pole):boolean;
var
  i,j:integer;
begin
  Result:=false;
  for i:=2 to N do
    for j:=1 to i-1 do
      if a[i,j]<>a[j,i] then exit;
  Result:=true;
end;
  • príkaz exit znamená okamžité ukončenie podprogramu - ako keby sa skočilo na posledné end podprogramu

Kopírujeme a modifikujeme obrázky

Aby sa nám lepšie natrénovala práca s dvojrozmerným poľom, vytvoríme si v Delphi nový projekt, v ktorom budú dve "grafické" plochy. Do prvej načítame z nejakého súboru bitmapu a túto potom prekopírujeme do druhej plochy. Pri kopírovaní ju môžeme rôzne meniť, prípadne meniť spôsoby kopírovania. S obrázkami budeme pracovať ako s dvojrozmerným poľom, pričom prvý index poľa bude vyjadrovať x-ovú súradnicu farebnej bodky (pixel). Bodky číslujeme od 0 do šírka-1.

  • Najprv si pripravme nový projekt -- do formulára vložíme dve grafické plochy Image, upravíme im rozmery, aby boli rovnako veľké 250x250
  • do formulára vložíme niekoľko tlačidiel -- postupne im budeme priraďovať rôzne funkcie (dvojkliknutím na príslušné tlačidlo) -- formulár teraz vyzerá nejako takto:

 

  • projekt uložíme do nejakého adresára na disku a tiež sem prekopírujeme niekoľko obrázkov (súborov s príponou .BMP) -- môžete si stiahnuť tieto súbory obrazky.zip
  • dvojklikneme na Button1 a priradíme mu akciu načítanie bitmapy napr. tiger.bmp do prvej grafickej plochy Image1:

do grafickej plochy sa načíta obrázok zo súboru:

procedure TForm1.Button1Click(Sender: TObject);
begin
  Image1.Picture.LoadFromFile('tiger.bmp');
end;
  • podobne môžeme tlačidlu Button2 priradiť nejakú inú bitmapu
  • tlačidlo Button4 bude slúžiť na kopírovanie obrázka z Image1 do plochy Image2
  • stavová premenná Pixels nám umožňuje pristupovať k jednotlivým pixelom (farebným bodkám) obrázka - funguje to na rovnakom princípe ako Strings v textovej ploche Memo - teda Pixels je ako dvojrozmerné pole prvkov typu TColor (farba)

postupné kopírovanie po riadkoch:

procedure TForm1.Button4Click(Sender: TObject);
var
  g1,g2:TCanvas;
  x,y:integer;
begin
  g1:=Image1.Canvas; g2:=Image2.Canvas;
  for y:=0 to Image1.Height-1 do begin
    for x:=0 to Image1.Width-1 do
      g2.Pixels[x,y]:=g1.Pixels[x,y];
    Image2.Repaint;
  end;
end;
  • Image2.Repaint prekreslí druhú grafickú plochu po prekopírovaní každého riadka -- inak by sa zobrazila až záverečná zmena
  • ďalší variant sa hrá s farbami cez ich položky RGB:

práca so zložkami farby bodov:

procedure TForm1.Button5Click(Sender: TObject);
var
  g1,g2:TCanvas;
  x,y:integer;
  r,g,b:byte;
  c:TColor;
begin
  g1:=Image1.Canvas; g2:=Image2.Canvas;
  for y:=0 to Image1.Height-1 do begin
    for x:=0 to Image1.Width-1 do begin
      c:=g1.Pixels[x,y];
      r:=GetRValue(c); g:=GetGValue(c) div 2; b:=GetBValue(c);
      g2.Pixels[x,y]:=RGB(r,g,b);
    end;
    Image2.Repaint;
  end;
end;
  • funkcia GetRValue vráti červenú zložku farby, podobne GetGValue a GetBValue vrátia zelenú a modrú zložku

Práca s myšou

  • v Delphi sa s myšou pracuje pomocou udalostí, ktoré automaticky vznikajú pri každom pohybe myši na obrazovke
    • pri pohybe myši túto udalosť dostáva ten komponent formulára, nad ktorým sa myš nachádza
    • procedúra, ktorá sa vyvolá ako reakcia na udalosť (event driver) pohyb myši, dostáva ako parametre aj súradnice myši (X,Y) - tieto sú vždy relatívne vzhľadom na ľavý horný roh komponentu
  • napíšeme procedúru, ktorá sa zavolá vždy, keď pohneme kurzorom myši nad plochou Image2 - táto procedúra prekopíruje len jednu bodku z Image1 a to presne z pozície myši
  • najprv vo formulári klikneme na Image2 a potom v Inšpektore Objektov (F11) prepneme záložku (kartu) Events a dvojklikneme na pravú časť riadka s OnMouseMove

  • pripraví sa procedúra TForm1.Image2MouseMove, do ktorej napíšeme:

spracovanie udalosti pri každom pohnutí myši nad plochou Image2:

procedure TForm1.Image2MouseMove(Sender:TObject; Shift:TShiftState; X,Y:Integer);
var
  g1,g2:TCanvas;
begin
  g1:=Image1.Canvas; g2:=Image2.Canvas;
  g2.Pixels[x,y]:=g1.Pixels[x,y];
end;
  • pričom X a Y sú súradnice bodu, nad ktorým sa nachádza kurzor myši
  • procedúru vylepšíme tak, že sa prekopíruje aj malé okolie bodu (X,Y):

vylepšená procedúra:

procedure TForm1.Image2MouseMove(Sender:TObject; Shift:TShiftState; X,Y:Integer);
const
  d=3;
var
  g1,g2:TCanvas;
  i,j:integer;
begin
  g1:=Image1.Canvas; g2:=Image2.Canvas;
  for i:=X-d to X+d do
    for j:=Y-d to Y+d do
      g2.Pixels[i,j]:=g1.Pixels[i,j];
end;
  • v Inšpektore Objektov môžeme jednoducho zmeniť aj popisy tlačidiel, napr. takto: vo formulári klikneme na Button1 (tým sa oselektuje), v inšpektore prepneme záložku Properties a zmeníme pravú časť riadka Caption na ľubovoľný text, napr. Tiger
  • môže sa vám stať, že grafická plocha pri každom prekresľovaní veľmi bliká, vtedy pomôže riadok, ktorý pridáme do procedúry FormCreate (táto procedúra sa vytvorí dvojkliknutím do formulára na prázdne miesto bez komponentu):

aby grafické plochy neblikali:

procedure TForm1.FormCreate(Sender: TObject);
begin
  DoubleBuffered:=true;
end;

NDÚ:

  • pri kopírovaní pixelov môžete posúvať začiatok každého riadka o nejakú vypočítanú hodnotu, napr. pomocou funkcie sin
  • obrázky môžete preklápať podľa rôznych osí
  • obrázok sa dá zväčšovať/zmenšovať, resp. kopírovať viackrát na rôzne pozície; v nejakej časti zahusťovať a pod.
  • je zaujímavé pohrať sa s farbami - robiť rôzne jasy, stmavovať obrázok ku okrajom plochy, zvýrazňovať len nejakú farebnú zložku (napr. "dozelena")
  • z obrázka prekopírovať len tie pixely, ktoré sú od nejakého bodu (napr. (150,150)) nie ďalej ako napr. 100 -- ostatné pixely zafarbiť na bielo
  • otočiť obrázok Image1 o 90 stupňov
    • otočenie Image1 na svojom mieste tak, že sa pritom nepoužijete Image2 (ani iný TImage) a ani iná dátová štruktúra (napr. pomocné pole)

Príklad s korytnačkou a editovacím riadkom

  • vytvoríme novú aplikáciu - do formulára položíme grafickú plochu (Image1) a pod ňu editovací riadok (komponent Edit1 zo štandardnej palety komponentov):
  • naprogramujeme takéto správanie:
    • v grafickej ploche bude korytnačka a túto budeme riadiť príkazmi, ktoré sa zapisujú do editovacieho riadka
    • v Inšpektore objektov sa nastavíme na komponent Edit1, prepneme Udalosti (Events) a dvojklikneme OnKeyPress - vytvorí sa metóda Edit1KeyPress - táto metóda sa zavolá vždy, keď sa bude niečo zapisovať do editovacieho riadka - nás však bude zaujímať len kláves <Enter> s kódom #13
    • program bude rozpoznávať túto množinu príkazov:
      • dopredu vlavo vpravo ph pd zmenfp zmenhp
      • parameter príkazu je od neho oddelený aspoň jednou medzerou
    • Edit1.Text je momentálny obsah editovacieho riadka
    • štandardná funkcia StrToIntDef prevedie reťazec na číslo a ak reťazec nie je správne zadané celé číslo, tak vráti druhý parameter funkcie (tzv. náhradnú hodnotu - default)
    • pomocou const môžeme definovať aj pole konštánt - za identifikátor konštanty napíšeme definíciu poľa a za znamienko rovnosti do zátvoriek postupne vymenujeme všetky konštanty

program:

var
  k:TKor;

procedure TForm1.FormCreate(Sender: TObject);
begin
  k:=TKor.Create;
  Edit1.Text:='';
end;

procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
const
  tab:array[1..7] of string =
   ('dopredu','vlavo','vpravo','ph','pd','zmenfp','zmenhp');
  farby:array[0..7] of TColor =
   (clBlack,clBlue,clRed,clGreen,clGray,clNavy,clYellow,clWhite);
var
  s:string;
  i,p:integer;
begin
  if Key<>#13 then exit;
  s:=Edit1.Text;
  i:=pos(' ',s+' ');
  p:=StrToIntDef(copy(s,i+1,MaxInt),0);
  s:=LowerCase(copy(s,1,i-1));
  i:=1; while (i<=high(tab)) and (tab[i]<>s) do inc(i);
  with k do
    case i of
      1: Dopredu(p);
      2: Vlavo(p);
      3: Vpravo(p);
      4: PH;
      5: PD;
      6: if (p>=low(farby)) and (p<=high(farby)) then
           ZmenFP(farby[p]);
      7: ZmenHP(p);
      else exit;
    end;
  Edit1.Text:='';
end;
  • program vylepšíme tak, že zabezpečíme, aby bolo korytnačku vidieť - nakreslíme ju zeleným trojuholníkom - trojuholník bude natočený v smere natočenia korytnačky - vždy, keď sa korytnačka pohne alebo otočí, prekreslí sa celá grafická plocha najprv bez korytnačky a na záver ju dokreslíme v novej polohe (pomocná procedúra kresli)
  • budeme si pamätať postupnosť všetkých doterajších príkazov (pole prikazy)
  • príkaz zmaz vyprázdni túto zapamätanú postupnosť

program:

var
  k:TKor;
  prikazy:array[1..1000] of record prikaz,param:integer; end;
  pocet:integer = 0;

procedure kresli;
const
  farby:array[0..7] of TColor =
   (clBlack,clBlue,clRed,clGreen,clGray,clNavy,clYellow,clWhite);
var
  i:integer;
begin
  with k do begin
    FP:=clBlack; HP:=1; ZmenXY(sirka/2,vyska/2); Smer:=0; PD;
    zmaz;
    for i:=1 to pocet do
      with prikazy[i] do
        case prikaz of
          1: Dopredu(param);
          2: Vlavo(param);
          3: Vpravo(param);
          4: PH;
          5: PD;
          6: if (param>=low(farby)) and (param<=high(farby)) then
               ZmenFP(farby[param]);
          7: ZmenHP(param);
          8: pocet:=0;
        end;
    // nakreslí rovnoramenný zelený trojuholník
    FP:=clLime; HP:=3; PD;
    Vpravo(90); Dopredu(8); Vlavo(105); Dopredu(30.9);
    Vlavo(150); Dopredu(30.9); Vlavo(105); Dopredu(8);
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  k:=TKor.Create;
  Edit1.Text:='';
  kresli;
end;

procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
const
  tab:array[0..7] of string =
   ('zmaz','dopredu','vlavo','vpravo','ph','pd','zmenfp','zmenhp');
var
  s:string;
  i,p:integer;
begin
  if Key<>#13 then exit;
  s:=Edit1.Text;
  i:=pos(' ',s+' ');
  p:=StrToIntDef(copy(s,i+1,MaxInt),0);
  s:=LowerCase(copy(s,1,i-1));
  i:=0; while (i<=high(tab)) and (tab[i]<>s) do inc(i);
  if i>high(tab) then exit;
  if i=0 then pocet:=0       // príkaz zmaz
  else begin
    inc(pocet); prikazy[pocet].prikaz:=i; prikazy[pocet].param:=p;
  end;
  kresli;
  Edit1.Text:=''; Key:=#0;   // editovací riadok sme úspešne spracovali
end;

NDÚ

  • namiesto editovacieho riadka použite textovú plochu, do ktorej sa zapisuje celá postupnosť korytnačích príkazov a celá sa spracováva až do konca alebo po prvý nedokončený, resp. nesprávny príkaz


© 2002 AB, KVI
blaho@fmph.uniba.sk