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;
}
}
}
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 zList
?