$$ \newcommand{\floor}[1]{\left\lfloor{#1}\right\rfloor} \newcommand{\ceil}[1]{\left\lceil{#1}\right\rceil} \renewcommand{\mod}{\,\mathrm{mod}\,} \renewcommand{\div}{\,\mathrm{div}\,} \newcommand{\metar}{\,\mathrm{m}} \newcommand{\cm}{\,\mathrm{cm}} \newcommand{\dm}{\,\mathrm{dm}} \newcommand{\litar}{\,\mathrm{l}} \newcommand{\km}{\,\mathrm{km}} \newcommand{\s}{\,\mathrm{s}} \newcommand{\h}{\,\mathrm{h}} \newcommand{\minut}{\,\mathrm{min}} \newcommand{\kmh}{\,\mathrm{\frac{km}{h}}} \newcommand{\ms}{\,\mathrm{\frac{m}{s}}} \newcommand{\mss}{\,\mathrm{\frac{m}{s^2}}} \newcommand{\mmin}{\,\mathrm{\frac{m}{min}}} \newcommand{\smin}{\,\mathrm{\frac{s}{min}}} $$

Prijavi problem


Obeleži sve kategorije koje odgovaraju problemu

Još detalja - opišite nam problem


Uspešno ste prijavili problem!
Status problema i sve dodatne informacije možete pratiti klikom na link.
Nažalost nismo trenutno u mogućnosti da obradimo vaš zahtev.
Molimo vas da pokušate kasnije.

Догађаји миша

На почетку поглавља о интеракцији смо поменули да постоје два основна начина да програм добије информације о акцијама корисника. Први начин је очитавање стања миша и тастатуре, и тај начин смо у међувремену упознали.

Очитавање стања миша и тастатуре је једноставно и за одређене примене то је управо оно што нам треба. Ипак, у неким ситуацијама оно није најпогоднији начин рада. На пример, ако треба да у програму реагујемо на клик мишем:

  • при сувише честом очитавању стања миша може се догодити да се у више узастопних очитавања констатује да је тастер миша доле, али не знамо да ли је то све исти клик или више кликова.

  • при ређем очитавању стања миша може се догодити да након једног очитавања корисник притисне и отпусти тастер пре следећег очитавања. У таквом случају програм неће добити информацију о том клику.

Другим речима, праћење стања тастера миша није најбољи начин да откријемо промену тог стања (а иста примедба важи и за тастатуру). Када смо пре свега заинтересовани за промене стања, боље је да пратимо догађаје, јер се они генеришу управо променом стања. На пример, при спуштању тастера миша оперативни систем добија сигнал од улазног уређаја и региструје то као догађај. Оперативни систем прослеђује информације о догађају апликацији која је у фокусу (то је апликација чији је прозор селектован и која тренутно прима улаз од миша и тастатуре). Апликација типа Windows-forms проналази контролу која је у фокусу и покреће одговарајућу функцију за обраду догађаја, ако таква функција постоји. Сви догађаји остају регистровани и запамћени до обраде, тако да се не може догодити да неку акцију корисника пропустимо, као што је случај када само очитавамо стање.

На нивоу контрола графичког интерфејса дефинисани су догађаји MouseDown и MouseUp, који одговарају спуштању и подизању тастера миша. То значи да са сваку контролу, као и за цео формулар, у прозору Properties могу да се пријаве функције за обраду ових догађаја. То се ради на исти начин као што смо то чинили у примерима из првог поглавља.

../_images/interact_MouseEvents.png

На слици видимо да се могу чак обрађивати и догађаји MouseClick и MouseDoubleClick, што нам је омогућила додатна логика, то јест алгоритми библиотеке .Net, који су у стању да секвенцу најједноставнијих догађаја (спуштање и подизање тастера миша) протумаче као сложенији догађај и да позову одговарајућу функцију за обраду, ако постоји.

Функције за обраду догађаја миша добијају параметар e, који садржи додатне информације о догађају. Информације које нас најчешће интересују су:

  • позиција миша у тренутку догађаја, коју можемо сазнати из целобројних својстава e.X и e.Y. Ако нам је потребна позиција као тачка, можемо да користимо и својство e.Location tипа Point, тако да не морамо сами да формирамо тачку.

  • тастери миша који су били притиснути у тренутку догађаја. Ова информација је садржана у својству e.Button типа MouseButtons. У лекцији Очитавање тастера и позиције миша смо објаснили како да из оваквог податка сазнамо који од тастера су били притиснути.

Брзи клик

У овом примеру правимо једноставну игрицу (једноставну за играње 😉). Круг се на екрану помера, а циљ играча (корисника) је да што више пута кликне на круг. За успешан клик се добија 10 поена, а за промашај -1 поен. Ако играч уопште не кликне мишем за време једног фрејма (до померања круга), добија -2 поена (губи 2 поена). За свако пропуштање клика и за сваки промашај губи се по један живот (на почетку има 5 живота).

После сваких 5 погодака ниво игре се повећава за 1 и она постаје тежа - круг се смањује и постаје бржи (интервали мировања су краћи). Са смањивањем и убрзавањем, круг уједно мења и боју од жуте ка црвеној.

Овде је потребно да корисник у току игре види све битне информације. Зато ћемо на формулар поставити три лабеле са натписима Nivo Poeni и Zivoti, а уз сваку од њих још по једну, у којој ће се приказивати тренутне вредности ових величина. Осим тога, треба нам дугме за покретање нове игре и (подразумева се) тајмер.

Да би игра лепше изгледала и да би део по коме се креће круг био одвојен од контрола, додаћемо и оквир за слику (PictureBox, користили смо га у примеру падања кише, где је ова контрола мало детаљније описана). Оквир за слику ћемо прислонити уз доњу ивицу формулара (подешавањем својства Dock), боју позадине мењамо тако да се оквир јасно разликује од остатка формулара. Формулар сад изгледа овако:

../_images/interact_ClickGame.png

У делу програмирања имаћемо и неколико помоћних функција:

  • У функцији PostaviNivo поред самог постављања вредности променљиве Nivo, подешавамо боју и величину круга и дужину интервала тајмера. Све ове вредности се одређују по неким погодно изабраним формулама у којима улествује редни број текућег нивоа.

  • У функцији ZivotManje смањујемо бројач живота и ако је то био поседњи живот, завршавамо текућу игру.

  • У функцији NoviCilj мењамо координате центра круга за малу случајну вредност (али тако да остане цео видљив), и рестартујемо тајмер да би време до новог фрејма почело да се броји испочетка и да би корисник имао времена да кликне.

Описаћемо укратко и функције за обраду догађаја:

  • У функцији pictureBox1_MouseClick (која обрађује клик на оквир за слику), проверавамо да ли је корисник кликнуо на круг. ОВо радимо тако што применом Питагорине теореме израчунамо растојање места клика (e.X, e.Y) до центра круга (X, Y) и то растојање упоредимо са величином круга. Ако је кликнуто у круг, додају се поени за погодак и по потреби ажурира ниво, у противном се одузимају поени за промашај и позива се помоћна функција ZivotManje.

  • У функцији btnNova_Click (која се извршава кад корисник кликне дугме за почињање игре) ресетујемо све вредности које описују стање игре и позивамо функцију NoviCilj да бисмо позиционирали круг.

  • У функцији timer1_Tick одузимамо поене за пропуштање клика и позивамо функције ZivotManje и NoviCilj

  • На крају, у функцији за цртање pictureBox1_Paint формирамо четку одговарајуће боје и цртамо круг. Обратите пажњу на то како се задаје боја четке на основу променљиве Boja, чија је вредност од 0 до 255. Смањивањем G компоненте док је R на максимуму а B на минимуму, добијамо нијансе од (255, 255, 0), што је жута, до (255, 0, 0), што је црвена.

using System;
using System.Drawing;
using System.Windows.Forms;

namespace BrzoKlik
{
    public partial class Form1 : Form
    {
        const int PoeniZaPogodak = 10;
        const int PoeniZaPromasaj = -1;
        const int PoeniZaPropustanje = -2;

        Random Rnd = new Random();

        bool IgraJeUToku = false;
        int BrojKrugova, Poeni, Zivoti;

        int Nivo = 1;
        int Velicina = 0, Boja = 0, Trajanje = 0;
        int X, Y;

        public Form1()
        {
            InitializeComponent();
            Text = "Igra refleksa";
            pictureBox1.BackColor = Color.FromArgb(23, 187, 156);
            X = pictureBox1.Width / 2;
            Y = pictureBox1.Height / 2;
        }

        private void PostaviNivo(int n)
        {
            Nivo = n;
            Velicina = Math.Max(10, 50 - 5 * n);
            Trajanje = Math.Max(500, 1000 - 50 * n);
            Boja = Math.Max(0, 255 - 30 * (n-1));

            lblNivo.Text = Nivo.ToString();
            timer1.Interval = Trajanje;
        }

        private void ZivotManje()
        {
            if (!IgraJeUToku)
                return;

            Zivoti--;
            lblZivoti.Text = Zivoti.ToString();
            if (Zivoti == 0)
            {
                Velicina = 0;
                timer1.Enabled = false;
                IgraJeUToku = false;
            }
        }

        private void NoviCilj()
        {
            X += Rnd.Next(-Velicina, Velicina);
            X = Math.Min(pictureBox1.Width - Velicina, Math.Max(Velicina, X));

            Y += Rnd.Next(-Velicina, Velicina);
            Y = Math.Min(pictureBox1.Height - Velicina, Math.Max(Velicina, Y));

            timer1.Enabled = false;
            timer1.Enabled = true;
            pictureBox1.Invalidate();
        }

        private void pictureBox1_MouseClick(object sender, MouseEventArgs e)
        {
            if (!IgraJeUToku)
                return;

            if ((e.X - X) * (e.X - X) + (e.Y - Y) * (e.Y - Y) <= Velicina * Velicina)
            {
                // Pogodak
                Poeni += PoeniZaPogodak;
                BrojKrugova++;
                if (BrojKrugova > 5 * Nivo)
                    PostaviNivo(Nivo + 1);
                NoviCilj();
            }
            else
            {
                // Promasaj
                Poeni += PoeniZaPromasaj;
                ZivotManje();
            }

            lblPoeni.Text = Poeni.ToString();
        }

        private void btnNova_Click(object sender, EventArgs e)
        {
            Poeni = 0;
            BrojKrugova = 0;
            Zivoti = 5;
            PostaviNivo(1);

            lblPoeni.Text = Poeni.ToString();
            lblZivoti.Text = Zivoti.ToString();
            IgraJeUToku = true;
            timer1.Enabled = true;

            NoviCilj();
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            if (!IgraJeUToku)
                return;

            // Propustanje
            Poeni += PoeniZaPropustanje;
            lblPoeni.Text = Poeni.ToString();
            ZivotManje();
            NoviCilj();
        }

        private void pictureBox1_Paint(object sender, PaintEventArgs e)
        {
            Brush cetka = new SolidBrush(Color.FromArgb(255, Boja, 0));
            e.Graphics.FillEllipse(cetka, X - Velicina, Y - Velicina, 2 * Velicina, 2 * Velicina);
        }

    }
}

Слагалица 15

Овај пример је још једна игра, овај пут позната. 15 бројева је приказано у табли од 4 реда и 4 колоне, а једно поље је празно. Корисник кликом на број који је поред празног поља помера тај број на празно поље. Циљ је да се бројеви сложе по реду (први ред 1-4 слева на десно, итд.) а последње (доње десно) поље да остане празно.

У насловној линији приказивати број одиграних потеза и протекло време. Када корисник реши слагалицу, приказати одговарајућу поруку, а на следећи клик започети нову игру.

../_images/interact_15puzzle.png

Централни објекат у овом задатку је матрица Slagalica величине 4 пута 4, у којој за свако поље пише који се број налази на њему (редови и колоне се у матрици броје од 0). Да бисмо брже проналазили положај празног поља, уместо да га тражимо по матрици (вредност му је 16), користимо променљиве RupaRed, RupaKol које садрже редни број реда и колоне празног поља.

Са становишта интеракције корисника са програмом, најинтересантнија је функција Form1_MouseClick. У случају да је игра у току, у овој функцији прво одређујемо којем пољу припада тачка на коју је корисник кликнуо, а затим покушавамо да то поље преместимо на празно поље (или да прместимо рупу на то поље, што ради функција PomeriRupu). Ако је померање успело, бројимо један потез и тражимо ново исцртавање слике. Ако игра није била у току, почињемо нову игру.

Функција timer1_Tick овде служи само да освежи насловну линију (испише актуелан број потеза и протекло време).

Функција Form1_Paint јесте нешто дужа него обично, али није превише компликована. Она у суштини у двострукој петљи приказује поља слагалице и исписује бројеве на тим пољима. У случају да игра није у току, ова функција исписује честитку кориснику за завршену претходну игру.

Помоћне функције (PomeriRupu, NovaIgra, SlagalicaJeSlozena) би требало да су јасне, а кључна места су прокоментарисана у коду, па их нећемо додатно описивати.

using System;
using System.Drawing;
using System.Windows.Forms;

namespace Slagalica15
{
    public partial class Form1 : Form
    {
        const int BrRedova = 4;
        const int BrKolona = 4;
        const int RUPA = BrRedova * BrKolona;
        int SirinaKolone;  // Sirina kolone u pikselima
        int VisinaReda;   // Visina reda u pikselima

        int[,] Slagalica = new int[BrRedova, BrKolona];
        int RupaRed, RupaKol;
        bool IgraJeUToku;
        int BrojPoteza, BrojSekundi;
        Random Rnd;

        public Form1()
        {
            InitializeComponent();
            ClientSize = new Size(400, 400);
            Text = "Slagalica";
            BackColor = Color.FromArgb(23, 187, 156);

            SirinaKolone = ClientSize.Width / BrKolona;
            VisinaReda = ClientSize.Height / BrRedova;

            // inicijalno slagalicu popunjavamo redom brojevima od 1 do n*n
            for (int red = 0; red < BrRedova; red++)
                for (int kol = 0; kol < BrKolona; kol++)
                    Slagalica[red, kol] = red * BrKolona + kol + 1;

            // prazno polje je u donjem desnom uglu
            RupaRed = BrRedova - 1;
            RupaKol = BrKolona - 1;

            Rnd = new Random();
            NovaIgra(); // pre početka igre mešamo slagalicu
        }

        private bool PomeriRupu(int r, int k)
        {
            if (0 <= r && r < BrRedova && 0 <= k & k < BrKolona) // ako je polje unutar slagalice
            {
                if (Math.Abs(r - RupaRed) + Math.Abs(k - RupaKol) == 1) // ako je polje susedno rupi
                {
                    Slagalica[RupaRed, RupaKol] = Slagalica[r, k];
                    Slagalica[r, k] = RUPA;
                    RupaRed = r;
                    RupaKol = k;
                    return true; // uspešno smo pomerili rupu
                }
            }
            return false; // nismo uspeli da pomerimo rupu
        }

        // nasumično mešamo slagalicu i resetuejmo brojace
        private void NovaIgra()
        {
            // četiri moguća smera pomeranja
            int[] smerRed = { -1, 1, 0, 0 };
            int[] smerKol = { 0, 0, -1, 1 };
            int preostaloPomeranja = 200;
            while (preostaloPomeranja > 0)
            {
                int iSmer = Rnd.Next(4);
                int susedRed = RupaRed + smerRed[iSmer];
                int susedKol = RupaKol + smerKol[iSmer];
                if (PomeriRupu(susedRed, susedKol)) // ako je pomeranje uspesno
                    preostaloPomeranja--; // imamo jedno manje do kraja
            }
            IgraJeUToku = true;
            BrojPoteza = 0;
            BrojSekundi = 0;
        }

        private bool SlagalicaJeSlozena()
        {
            // proveravamo da li postoji polje na kom se nalazi pogrešan broj
            for (int red = 0; red < BrRedova; red++)
                for (int kol = 0; kol < BrKolona; kol++)
                    if (Slagalica[red, kol] != red * BrKolona + kol + 1)
                        return false;  // broj na polju [red, kol] je pogrešan
            return true;  // nismo naišli na pogrešan broj
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            if (IgraJeUToku)
            {
                BrojSekundi++;
                Text = string.Format("Slagalica: vreme {0} potezi {1}", BrojSekundi, BrojPoteza);
            }
        }

        private void Form1_MouseClick(object sender, MouseEventArgs e)
        {
            if (IgraJeUToku)
            {
                int r = e.Y / VisinaReda;   // red na koji je kliknuto
                int k = e.X / SirinaKolone; // kolona na koju je kliknuto
                if (PomeriRupu(r, k))
                {
                    BrojPoteza++;
                    Invalidate(); // ako smo uspeli da pomerimo rupu na to polje, treba ponovo da crtamo
                    if (SlagalicaJeSlozena())
                        IgraJeUToku = false;
                }
            }
            else
            {
                NovaIgra();
                Invalidate();
            }
        }

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            Graphics g = e.Graphics;
            Font f = new Font("Arial", 40);
            Pen olovka = new Pen(Color.Black, 2);
            Brush cetka = new SolidBrush(Color.Black);
            StringFormat sf = new StringFormat
            {
                LineAlignment = StringAlignment.Center,
                Alignment = StringAlignment.Center
            };

            Text = string.Format("Slagalica: vreme {0} potezi {1}", BrojSekundi, BrojPoteza);
            if (IgraJeUToku)
            {
                // Prikazujemo stanje u igri
                for (int red = 0; red < BrRedova; red++)
                {
                    for (int kol = 0; kol < BrKolona; kol++)
                    {
                        int x = kol * SirinaKolone;
                        int y = red * VisinaReda;
                        Rectangle r = new Rectangle(x, y, SirinaKolone, VisinaReda);
                        g.DrawRectangle(olovka, r);
                        if (Slagalica[red, kol] != RUPA)
                            g.DrawString(Slagalica[red, kol].ToString(), f, cetka, r, sf);
                        else
                            g.FillRectangle(cetka, r);
                    }
                }
            }
            else
            {
                e.Graphics.DrawString("Bravo!", f, cetka, ClientRectangle, sf);
            }
        }
    }
}