Programowanie w języku C# » Artykuły

Wypełnianie ekranu zbiorem obrazków. Dwa sposoby i przydatna transformacja

Wstęp


Artykuł dotyczy C# Windows Forms. Zwykle kontakt początkujących adeptów programowania w C# zaczyna się właśnie od tego składnika Visual Studio. Często spotyka się osoby, które chcą od razu zrobić prosty program z grafiką, prostą grę, czy wygaszacz ekranu. Od poznania podstaw i poniższych dwóch programów można zacząć.

W części głównej pokazano różnicę w sposobie tworzenia i w działaniu dwóch algorytmów, z których jeden umieszcza na formie kontrolki, na których są obrazki, a drugi robi to samo w konstruktorze formy i w metodzie Form_Paint.  Na formie i kontrolce na początku nie ma żadnych innych komponentów.

Dodatkowym elementem jest specyficzne dopasowywanie obrazka do prostokąta tła na którym obrazek się znajdzie.

Użyto przekształcenia równoległoboku (ang. parallelogram) pozwalającego na szybkie transformacje obrazów, nie tylko w kwadraty, czy prostokąty, ale też w równoległoboki, których 2 naprzeciwległe kąty są ostre, a dwa pozostałe rozwarte, bo kwadrat i prostokąt mają, jak wiadomo wszystkie kąty proste i to szczególne przypadki równoległoboku.

Zmiana choćby jednej współrzędnej z trzech definiujących równoległobok pokaże zmianę w zachowaniu się przekształcenia. Można to zrobić np. w programie tylko z formą i metodą Form_Paint.

Programy nie mają zastosowania do zdjęć pobranych wprost z komórki, czy aparatu fotograficznego, gdzie występują współcześnie ogromne rozdzielczości. Jeśli ktoś chce takie zdjęcia obejrzeć w poniższym programie można to zrobić dopiero po proporcjonalnym zmniejszeniu zdjęć np. w programie Paint – Zmień rozmiar, aby wyświetlanie nie trwało w nieskończoność. Zmniejszanie i przycięcie zdjęć przed umieszczeniem ich na stronach internetowych jest znaną techniką i można się o tym czasem przekonać próbując wykorzystać zdjęcie z dowolnego portalu, jako tło pulpitu. Nie zawsze jest odpowiednio duże.

Fragment związany z pomiarem czasów wyświetlania może się przydać przy testowaniu różnych algorytmów, różnych formatów i plików z obrazkami i różnych rozmiarów obrazów.

Efekt końcowy (fragment zrzutu ekranu)




Efekt "artystyczny" po ingerencji we wzory obliczające współrzędne punktów równoległoboku




Proste przykłady malowania równoległoboków


W celu zapoznania się z podstawami należy poniższą metodą zastąpić metodę Form_Paint autora i umieścić jakiś obrazek w folderze bin projektu VisualStudio C# (lub po prostu w folderze exe-ka).
Pokazane w późniejszym kodzie autora ścieżki do obrazków są specyficzne dla układu folderów i plików w projekcie autora.
Następną czynnością jest branie w komentarze fragmentów obejrzanych i wydobywanie z komentarzy fragmentów kolejnych.
Uwaga: fragmentów tych jest kilka w kodzie; nie wystarczy zamiana komentarzy tylko tam, gdzie są współrzędne x, y, x1, y1, x2, y2, czyli na początku.

void Form_Paint(object sender, PaintEventArgs e)
{
    string file = Application.StartupPath + @"\obrazek.png";
    Bitmap b = new Bitmap(file);
 
    /*
    Początek układu współrzędnych tak, jak współrzędne graficzne ekranu (X, Y) = (0, 0).
    Strzałka dodatnia osi X = (ClientRectangle.Right, 0)
    Strzałka dodatnia osi Y = (0, ClientRectangle.Bottom)
    */
 
    // kwadrat
    int x = 200;
    int y = 200;
 
    // prostokąt
    // int x = 400; 
    // int y = 200;
 
    // romb
    // int x1 = 200, y1 = 50;
    // int x2 = 50, y2 = 200;
 
    // rownoleglobok dowolny
    // int x1 = 400, y1 = 50;
    // int x2 = 100, y2 = 200;
 
    /*
    Początek układu współrzędnych w punkcie (X, Y) = (ClientRectangle.Right, 0).
    Strzałka ujemna osi X = (0, 0)
    Strzałka dodatnia osi Y = (ClientRectangle.Bottom, 0)
    */
 
    // romb po prawej stronie ekranu
    // int x1 = ClientRectangle.Right - 200, y1 = 50;
    // int x2 = ClientRectangle.Right - 50, y2 = 200;
 
    Rectangle srcRect = new Rectangle(0, 0, b.Width, b.Height);
 
    // po lewej stronie ekranu
    Point LeftTop = new Point(0, 0); // w późniejszym kodzie lt
 
    // po prawej stronie ekranu
    // Point LeftTop = new Point(ClientRectangle.Right, 0);
 
    // kwadrat, prostokąt
    Point RightTop = new Point(x, 0); //  w późniejszym kodzie rt
    Point LeftBottom = new Point(0, y);
 
    // romb, rownoleglobok dowolny
    // Point RightTop = new Point(x1, y1);
    // Point LeftBottom = new Point(x2, y2); //  w późniejszym kodzie lb
 
    Point[] destParallelogram = { LeftTop, RightTop, LeftBottom };
    e.Graphics.DrawImage(b, destParallelogram, srcRect, GraphicsUnit.Pixel);
}

Kwadrat




Prostokąt




Romb




Równoległobok dowolny




Romb po prawej stronie ekranu




Algorytm z obrazkami na kafelkach-kontrolkach


Moduł formy


using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using nsCommon;
 
namespace nsAppCX
{
    public partial class FormCX : Form
    {
        public FormCX()
        {
            ArrayList files = new ArrayList();
 
            Text = "CX";
            Common.FormText = Text;
 
            // Utworzenie okna na cały ekran z wyłączeniem paska zadań
 
            WindowState = FormWindowState.Normal; 
            StartPosition = FormStartPosition.Manual;
            Location = new Point(0, 0);
            Rectangle wa = Screen.PrimaryScreen.WorkingArea;
            Size = new Size(wa.Width, wa.Height);
 
            // Obliczenie ile będzie kafelków w poziomie i w pionie. Rozmiary kafelków są stałe.
 
            int horzCount = ClientSize.Width / TileCtrlCX.width; 
            int vertCount = ClientSize.Height / TileCtrlCX.height; 
 
            // Start pomiaru czasu
 
            Common.TimeProcRun = DateTime.Now; 
 
            // Wczytanie nazw plików z dysku z jednoczesnym przefiltrowaniem tylko obrazków
 
            try 
            {
                files.AddRange(Common.GetFiles(Application.StartupPath + @"\..\..\..\img", "*.jpg|*.png|*.bmp"));
            }
            catch (IOException ex)
            {
                MessageBox.Show(ex.Message);
            }
 
 
            if (files.Count > 0) 
            {
                // Utworzenie listy bitmap w oparciu o nazwy plików
 
                List<Bitmap> bitmaps = new List<Bitmap>(); 
 
                foreach (string fn in files) 
                    bitmaps.Add(new Bitmap(fn));
 
                // Tworzenie kafelków z bitmapami i dodawanie do formy
 
                for (int i = 0, y = 0; y < vertCount; y++) 
                    for (int x = 0; x < horzCount; i = ++i % files.Count, x++)
                    {
                        TileCtrlCX t = new TileCtrlCX(x, y, bitmaps[i]);                        
                        Controls.Add(t);
                    }
            }
 
            // Koniec pomiaru czasu
 
            Common.TimeProcEnd = DateTime.Now;
 
            //Ewentualny log do wykorzystania po narysowaniu
 
            //FormClosing += new FormClosingEventHandler(Common.FormClosing);
        }
    }
}


Moduł kafelka


using System.Drawing;
using System.Windows.Forms;
 
namespace nsAppCX
{
    public partial class TileCtrlCX : UserControl
    {
        public static readonly int width = 146;
        public static readonly int height = 121;
 
        public TileCtrlCX(int x, int y, Bitmap b)
        {
            Location = new Point(x * width, y * height);
            Size = new Size(width, height);
 
            // Tu chodzi tylko o przygotowanie zbioru punktów, a nie o równoległobok i jego transformację
 
            Point lt = new Point(x * width, y * height);
            Point rt = new Point(b.Width * height / b.Height + x * width, height);
            Point lb = new Point(x * width, (y + 1) * height);
 
            Rectangle r = new Rectangle(0, 0, rt.X - lt.X,  lb.Y - lt.Y);
 
            Bitmap b2 = new Bitmap(b, r.Width, r.Height);
 
            if (b.Width > b.Height)
                BackgroundImageLayout = ImageLayout.Tile;
            else
                BackgroundImageLayout = ImageLayout.None;
 
            BackgroundImage = b2;
 
            if ((x + y) % 2 == 0)
                BackColor = Color.LightGray;
            else
                BackColor = Color.SkyBlue;
        }
    }
}


Algorytm korzystający tylko z metody Form_Paint


Moduł formy


using System;
using System.Collections;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using nsCommon;
 
namespace nsAppCY
{
    public partial class FormCY : Form
    {
        const int width = 146;
        const int height = 121;
 
        Bitmap[] bitmaps;
        ArrayList files = new ArrayList();
 
        public FormCY()
        {
            Text = "CY";
            Common.FormText = Text;
 
            // Utworzenie okna na cały ekran z wyłączeniem paska zadań
 
            WindowState = FormWindowState.Normal;
            StartPosition = FormStartPosition.Manual;
            Location = new Point(0, 0);
            Rectangle wa = Screen.PrimaryScreen.WorkingArea;
            Size = new Size(wa.Width, wa.Height);
 
            // Start pomiaru czasu
 
            Common.TimeProcRun = DateTime.Now;
 
            // Wczytanie nazw plików z dysku z jednoczesnym przefiltrowaniem tylko obrazków
 
            try
            {
                files.AddRange(Common.GetFiles(
                    Application.StartupPath + @"\..\..\..\img", "*.jpg|*.png|*.bmp"
                    ));
            }
            catch (IOException ex)
            {
                MessageBox.Show(ex.Message);
            }
 
            if (files.Count > 0)
            {
                // Tablica na bitmapy
 
                bitmaps = new Bitmap[files.Count];
                int bmpIdx = 0;
 
                // Utworzenie bitmap w oparciu o nazwy plików
 
                foreach (string file in files)
                    bitmaps[bmpIdx++] = new Bitmap(file);
 
                // Dodanie obsługi zdarzenia Form_Paint
 
                Paint += new PaintEventHandler(Form_Paint);
 
                //Ewentualny log do wykorzystania po narysowaniu
 
                //FormClosing += new FormClosingEventHandler(Common.FormClosing);
            }
        }
 
        void Form_Paint(object sender, PaintEventArgs e)
        {
            int horzCt = ClientSize.Width / width;
            int vertCt = ClientSize.Height / height;
 
            for (int i = 0, y = 0; y < vertCt; y++)
                for (int x = 0; x < horzCt; i = ++i % files.Count, x++)
                {
                    float f = (bitmaps[i].Width * height) / (bitmaps[i].Height * width);
                    Rectangle srcRect = new Rectangle(0, 0, bitmaps[i].Width, bitmaps[i].Height);
 
                    // Sposób korzystania z przekształceń równoległoboku
 
                    // https://msdn.microsoft.com/en-us/library/aa327525(v=vs.71).aspx
 
                    Point lt = new Point(x * width, y * height);
                    Point rt = new Point(bitmaps[i].Width * height / bitmaps[i].Height + x * width, y * height);
                    Point lb = new Point(x * width, (y + 1) * height);
 
                    // 2 linijki specyficzne dla sposobu wyświetlania, który tu pokazano
 
                    if (rt.Y - lt.Y > height)
                        lt.Y += (rt.Y - lt.Y - height) / 4;
 
                    Point[] destParallelogram = { lt, rt, lb };
 
                    e.Graphics.Clip = new Region(new Rectangle(lt.X, lb.Y - height, width, height));
                    e.Graphics.FillRectangle(((x + y) % 2 == 0) ? Brushes.LightGray : Brushes.SkyBlue, lt.X, lb.Y - height, width, height);
                    e.Graphics.DrawImage(bitmaps[i], destParallelogram, srcRect, GraphicsUnit.Pixel);
                }
 
            // Koniec pomiaru czasu
 
            Common.TimeProcEnd = DateTime.Now;
        }
    }
}


Wspólny moduł dla obu algorytmów


using System;
using System.Collections;
using System.Diagnostics;
using System.IO;
using System.Windows.Forms;
 
namespace nsCommon
{
    class Common
    {
        public static string FormText;
        public static DateTime TimeProcRun;
        public static DateTime TimeProcEnd;
 
        // Funkcja pobierająca listę nazw plików w oparciu wspólną listę filtrów
 
        public static string[] GetFiles(string dir, string filter)
        {
            // http://www.beansoftware.com/ASP.NET-FAQ/Multiple-Filters-Directory.GetFiles-Method.aspx
 
            ArrayList files = new ArrayList();
            string[] Filters = filter.Split('|');
 
            foreach (string Filter in Filters)
                files.AddRange(Directory.GetFiles(dir, Filter, SearchOption.TopDirectoryOnly));
 
            return (string[])files.ToArray(typeof(string));
        }
 
        // Pomocnicza funkcja zapisująca log - czasy i minimum informacji o tym, 
        // który program uruchamiano i na jakim systemie Windows
 
        public static void FormClosing(object sender, FormClosingEventArgs e)
        {
            string fnLog = Application.StartupPath + @"\..\..\..\Log.txt";
            StreamWriter sw;
 
            TimeSpan ts = TimeProcEnd.Subtract(TimeProcRun);
            int t = (ts.Seconds * 1000) + ts.Milliseconds;
 
            try
            {
                /*
                if (!File.Exists(fnLog))
                    sw = File.CreateText(fnLog);
                else
                    sw = File.AppendText(fnLog);
                 */
 
                sw = File.CreateText(fnLog);
 
                sw.WriteLine("\nLog {0}", DateTime.Now.ToString("dd/MM/yyyy hh:mm:ss"));
                sw.WriteLine("System   {0}", OpSys());
                sw.WriteLine("Program  {0}", FormText);
                sw.WriteLine("ProcRun  {0} s", Common.TimeProcRun.ToString("ss.fff"));
                sw.WriteLine("ProcEnd  {0} s", Common.TimeProcEnd.ToString("ss.fff"));
                sw.WriteLine("ProcTime {0} ms", t);
                sw.WriteLine("End.");
                sw.Close();
            }
            catch (IOException ex)
            {
                MessageBox.Show(ex.Message);
            }
 
            // Wyswietlenie loga zaraz po narysowaniu
 
            Process p = new Process();
            p.StartInfo.FileName = fnLog;
            p.Start();
        }
 
        public static string OpSys()
        {
            OperatingSystem os = Environment.OSVersion;
            string opSys = "non-consider";
 
            if (os.Platform == PlatformID.Win32NT && os.Version.Minor != 0)
            {
                switch (os.Version.Major)
                {
                    case 5:
                        opSys = "WinXP";
                        break;
                    case 6:
                        opSys = "Win7";
                        break;
                }
            }
 
            return opSys;
        }
    }
}

2 komentarze

Artur Protasewicz 2016-02-15 16:12

W sumie mógbłym pisać List<string> zamiast ArrayList. To zaszłość historyczna z czasu nauki C#.
Historia jest taka, że kiedyś szukałem odpowiednika TStringList z Delphi i znalazłem ArrayList i tak już zostało.
List<T> było mi znacznie później potrzebne.
W programach powyżej jest użyte ArrayList do nazw plików, czyli stringów, a List do Bitmap, czyli obrazków
Stosuję zwykle takie odróżnienie:
ArrayList - stringi
List<T> - dowolne typy T, gdzie string jest przypadkiem szczególnym
Dla mnie to bardziej przejrzyste.

some_ONE 2016-02-15 13:09

Dlaczego w niektórych miejscach korzystasz z ArrayList zamiast z List ?