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)

zrzut_ogolny.png

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

skosy.png

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

kwadrat.png

Prostokąt

prostokat.png

Romb

romb.png

Równoległobok dowolny

rownoleglobok.png

Romb po prawej stronie ekranu

romb_lustro.png

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 komentarzy

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.

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