Blog Mateusza Chodyły czyli interdyscyplinarnego .NETowca 8-)

Część 7.2 (Globalization, drawing, text manipulation; rysowanie)

2. kwietnia 2010 23:03

Wymagania:

  • Klasy: Brush, Brushes, SystemBrushes, TextureBrush, SolidBrush, Pen, Pens, SystemPens, ColorConverter, ColorTranslator, SystemColors, StringFormat, Font, FontConverter, FontFamily, SystemFonts, Graphics, BufferedGraphics, BufferedGraphicsManager, Image, ImageConverter, ImageAnimator, Bitmap, Icon, IconConverter, SystemIcons, PointConverter, RectangleConverter, SizeConverter, Region,
  • Struktury: Color, Point, Rectangle, Size.

Mamy więc do czynienia z “klasyką” - WPF (Windows Presentation Foundation) nie jest wymagany.

W centrum tego rozdziału znajduje się klasa Graphics. Hermetyzuje ona powierzchnię rysowania GDI+ (dostarcza metod do rysowania obiektów). Możesz uzyskać dostęp do obiektu tej klasy na 3 sposoby:

  • przez wywołanie Control.CreateGraphics na obiekcie dziedziczącym z System.Windows.Forms.Control,
  • przez obsługę zdarzenia Control.Paint i dostęp do własności Graphics klasy System.Windows.Forms.PaintEventArgs,
  • z obrazka przez użycie metody FromImage.

Ogólnie rzecz biorąc (dużo pomijam):

  • DrawArc/Bezier(s)/ClosedCurve/Curve/Ellipse/Icon/IconUnstretched/Image/ImageUnscaled/ DrawImageUnscaledAndClipped/Line(s)/Path/Pie/Polygon/Rectangle(s)/String - wiadomo,
  • FillClosedCurve/Ellipse/Path/Pie/Polygon/Rectangle(s)/Region - też wiadomo,
  • public SizeF MeasureString(string text, Font font) - mierzy określony string podczas rysowania określonym obiektem klasy Font,
  • public Region Clip { get; set; } - ogranicza region rysowania (sprawdź Region.IsInfinite),
  • public RectangleF ClipBounds { get; }.

Przykład:

public partial class Form1 : Form
{
  public Form1()
  {
    InitializeComponent();

    this.Paint += new PaintEventHandler(Form1_Paint);
    this.Resize += new EventHandler(Form1_Resize);

    this.BackColor = Color.FromArgb(10, 200, 200); // jest też przeładowanie (alpha, red, green, blue)
  }

  void Form1_Resize(object sender, EventArgs e)
  {
    Invalidate();
  }

  void Form1_Paint(object sender, PaintEventArgs e)
  {
    Pen myPen = new Pen(Color.Red);
    e.Graphics.DrawLine(myPen, new Point(0, 0), new Point(this.ClientSize.Width, this.ClientSize.Height));

    Graphics g = this.CreateGraphics();
    g.FillRectangle(new SolidBrush(Color.Aqua), new Rectangle(0, 0, 50, 50));

    Bitmap myBitmap = new Bitmap(Environment.CurrentDirectory + "\\bitmapa.bmp"); // tylko nieindeksowane pliki .bmp
    g = Graphics.FromImage(myBitmap);
    g.DrawCurve(new Pen(Color.Brown, (float)2.5), new Point[] { new Point(5, 5), new Point(30, 30), new Point(55,5) });
    e.Graphics.DrawImage(myBitmap, new PointF(50.0F, 50.0F));

    myPen.DashStyle = System.Drawing.Drawing2D.DashStyle.DashDotDot;
    myPen.Width = 4;
    myPen.StartCap = System.Drawing.Drawing2D.LineCap.DiamondAnchor;
    myPen.EndCap = System.Drawing.Drawing2D.LineCap.Triangle;
    e.Graphics.DrawLine(myPen, new Point(0, 200), new Point(this.ClientSize.Width, 200));

    g = this.CreateGraphics();
    Pen p = new Pen(Color.Maroon, 2);
    Brush b = new LinearGradientBrush(new Point(1, 1), new Point(100, 100), Color.White, Color.Red);
    Point[] points = new Point[] {new Point(10, 210), new Point(10, 300), new Point(50, 265), new Point(100, 300), new Point(85, 240)};
    g.FillPolygon(b, points);
    g.DrawPolygon(p, points);

    //g.Clear(Color.DarkGreen); // czyszczenie i wypełnienie całego obszaru
  }
}

W powyższym przykładzie wykorzystałem zarówno klasę Pen, jak i Brush. Pierwsza jest potrzebna oczywiście przy rysowaniu linii, natomiast druga do wypełniania. Klasa Brush jest abstrakcyjna i dziedziczą z niej:

System.Drawing.SolidBrush (jeden kolor)
System.Drawing.TextureBrush (użycie obrazka do wypełnienia wnętrza)
System.Drawing.Drawing2D.HatchBrush ("kreskowanie/cieniowanie w kratkę": styl, kolor pierwszego planu i tła)
System.Drawing.Drawing2D.LinearGradientBrush
System.Drawing.Drawing2D.PathGradientBrush (wypełnia wnętrze obiektu GraphicsPath gradientem)

Krótki przykład:

void Form1_Paint(object sender, PaintEventArgs e)
{
  Pen skyBluePen = new Pen(Brushes.DeepSkyBlue);
  skyBluePen.Width = 8.0F;
  skyBluePen.LineJoin = System.Drawing.Drawing2D.LineJoin.Bevel;
  e.Graphics.DrawRectangle(skyBluePen, new Rectangle(10, 10, 75, 100));
  skyBluePen.Dispose();
  
  Bitmap image1 = (Bitmap)Image.FromFile(Environment.CurrentDirectory + "\\bitmapa.bmp", true);
  TextureBrush texture = new TextureBrush(image1);
  texture.WrapMode = System.Drawing.Drawing2D.WrapMode.Tile;
  e.Graphics.FillEllipse(texture, new RectangleF(90.0F, 110.0F, 100, 100));
}

Aby ułatwić sobie jeszcze bardziej życie można wykorzystać statyczne własności z klas Brushes i Pens (zwracają instancje w kolorze określonym przez nazwę własności). Można także wykorzystać klasy:

  • SystemBrushes - każda własność jest typu SolidBrush, który jest element wyświetlanym przez Windows; można też wykorzystać poniższą metodę: Brush theBrush = SystemBrushes.FromSystemColor(SystemColors.Highlight),
  • SystemPens - analogicznie (każda własność tej klasy jest typu Pen, szerokość 1 pixel), np. e.Graphics.DrawLine(SystemPens.Highlight, startPoint, endPoint);

Klasa ColorConverter konwertuje kolory z jednego typu danych na inny (dostęp przez klasę TypeDescriptor - dostarcza informacji o charakterystykach komponentu, m.in. atrybuty, własności i zdarzenia). ColorConverter oczekuje samej nazwy - tzn. np. Blue, a nie System.Drawing.Color.Blue. Klasa ColorTranslator tłumaczy kolory na i ze struktur Color GDI+. W klasie SystemColors każda własność to struktura Color, która reprezentuje wyświetlany element Windows. Dla lepszej wydajności użyj SystemPens lub SystemBrushes.

public Form1()
{
  InitializeComponent();

  this.Paint += new PaintEventHandler(Form1_Paint);
  this.Resize += new EventHandler(Form1_Resize);

  Color myColor = Color.Red;
  int winColor = ColorTranslator.ToWin32(myColor);
  MessageBox.Show(winColor.ToString()); // 255
  MessageBox.Show(ColorTranslator.ToWin32(Color.Yellow).ToString()); // 65535
  string htmlColor = ColorTranslator.ToHtml(myColor);
  MessageBox.Show(htmlColor); // Red
}

void Form1_Paint(object sender, PaintEventArgs e)
{
  Color myColor = Color.PaleVioletRed;
  System.ComponentModel.TypeConverter converter = System.ComponentModel.TypeDescriptor.GetConverter(myColor);
  string colorAsString = converter.ConvertToString(Color.PaleVioletRed);
  e.Graphics.DrawString(colorAsString, this.Font, Brushes.PaleVioletRed, 50.0F, 50.0F);
}

Przed przejściem do dalszych zagadnień warto zatrzymać się nad klasą BufferedGraphics. Dostarcza ona buforu dla podwójnego buforowania. Oznacza to, że można zredukować lub wyeliminować całkowicie efekt migotania (flicker), który jest wywoływany przez przerysowywanie wyświetlanej powierzchni. Podczas użycia podwójnego buforowania, aktualizowana grafika jest najpierw zapisywana do buforu w pamięci i później jego zawartość jest szybko wypisywana do wyświetlanej powierzchni (domyślnie ustawiona jest flaga OptimizedDoubleBuffer w metodzie Control.SetStyle). Klasa ta nie ma publicznego konstruktora i musi być tworzona przez BufferedGraphicsContext dla domeny aplikacji używając metody Allocate. Własność Graphics - rysowanie do buforu, metoda Render - rysowanie zawartości buforu. Przykład:

public partial class Form1 : Form
{
  private BufferedGraphicsContext context;
  private BufferedGraphics grafx;

  private byte bufferingMode;
  private string[] bufferingModeStrings = 
      { "Draw to Form without OptimizedDoubleBufferring control style",
        "Draw to Form using OptimizedDoubleBuffering control style",
        "Draw to HDC for form" };

  private System.Windows.Forms.Timer timer1;
  private byte count;

  public Form1() : base()
  {
    this.MouseDown += new MouseEventHandler(this.MouseDownHandler);
    this.Resize += new EventHandler(this.OnResize);
    this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true);

    timer1 = new System.Windows.Forms.Timer();
    timer1.Interval = 200;
    timer1.Tick += new EventHandler(this.OnTimer);

    bufferingMode = 2;
    count = 0;

    context = BufferedGraphicsManager.Current; // jedyna własność, pobiera BufferedGraphicsContext dla bieżącej domeny aplikacji
    context.MaximumBuffer = new Size(this.Width + 1, this.Height + 1);
    grafx = context.Allocate(this.CreateGraphics(), new Rectangle(0, 0, this.Width, this.Height));

    DrawToBuffer(grafx.Graphics);
  }

  private void MouseDownHandler(object sender, MouseEventArgs e)
  {
    if (e.Button == MouseButtons.Right)
    {
      if (++bufferingMode > 2)
        bufferingMode = 0;

      if (bufferingMode == 1)
        this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true); // ustawienie określonej flagi na true lub false

      if (bufferingMode == 2)
        this.SetStyle(ControlStyles.OptimizedDoubleBuffer, false);

      count = 6;
      DrawToBuffer(grafx.Graphics);
      this.Refresh(); // unieważnienie powierzchni klienta i przerysowanie
    }
    else
    {
      if (timer1.Enabled)
        timer1.Stop();
      else
        timer1.Start();
    }
  }

  private void OnTimer(object sender, EventArgs e)
  {
    DrawToBuffer(grafx.Graphics);

    if (bufferingMode == 2)
      grafx.Render(Graphics.FromHwnd(this.Handle));
    else
      this.Refresh();
  }

  private void OnResize(object sender, EventArgs e)
  {
    context.MaximumBuffer = new Size(this.Width + 1, this.Height + 1);
    if (grafx != null)
    {
      grafx.Dispose();
      grafx = null;
    }

    grafx = context.Allocate(this.CreateGraphics(), new Rectangle(0, 0, this.Width, this.Height));

    count = 6;
    DrawToBuffer(grafx.Graphics);
    this.Refresh();
  }

  private void DrawToBuffer(Graphics g)
  {
    if (++count > 5)
    {
      count = 0;
      grafx.Graphics.FillRectangle(Brushes.Black, 0, 0, this.Width, this.Height);
    }

    Random rnd = new Random();
    for (int i = 0; i < 20; i++)
    {
      int px = rnd.Next(20, this.Width - 40);
      int py = rnd.Next(20, this.Height - 40);
      g.DrawEllipse(new Pen(Color.FromArgb(rnd.Next(0, 255), rnd.Next(0, 255), rnd.Next(0, 255)), 1), px, py, px + rnd.Next(0, this.Width - px - 20), py + rnd.Next(0, this.Height - py - 20));
    }

    g.DrawString("Buffering Mode: " + bufferingModeStrings[bufferingMode], new Font("Arial", 8), Brushes.White, 10, 10);
    g.DrawString("Right-click to cycle buffering mode", new Font("Arial", 8), Brushes.White, 10, 22);
    g.DrawString("Left-click to toggle timed display refresh", new Font("Arial", 8), Brushes.White, 10, 34);
  }

  protected override void OnPaint(PaintEventArgs e)
  {
    grafx.Render(e.Graphics);
  }    
}

Kolejny przykład:

public Form1()
{
  InitializeComponent();

  this.SetStyle(ControlStyles.ResizeRedraw | ControlStyles.OptimizedDoubleBuffer, true);

  this.Paint += new PaintEventHandler(Form1_Paint);
  //this.Resize += new EventHandler(Form1_Resize); // niepotrzebne 
}

void Form1_Paint(object sender, PaintEventArgs e)
{
  e.Graphics.DrawLine(SystemPens.ActiveCaption, 0, 0, this.ClientSize.Width, this.ClientSize.Height);
  LinearGradientBrush myBrush = new LinearGradientBrush(new Rectangle(0, 0, this.ClientSize.Width, this.ClientSize.Height), Color.Red, Color.Yellow, LinearGradientMode.Horizontal);
  Pen myPen = new Pen(myBrush, 4);
  e.Graphics.DrawLine(myPen, new Point(this.ClientSize.Width, 0), new Point(0, this.ClientSize.Height));
}

void Form1_Resize(object sender, EventArgs e)
{
  Invalidate();
}

Kolejnym tematem jest zabawa obrazkami. GDI+ obsługuje poniższe formaty plików:

  • BMP - standardowy format Windows używany do przechowywania obrazków niezależnie od urządzenia i aplikacji, z reguły pliki nieskompresowane, często 24 bity na pixel,
  • GIF (Graphics Interchange Format) - dobry do rysowania linii, obrazków z blokami jednolitego koloru (ostrymi granicami pomiędzy kolorami), bezstratnie skompresowany, najwyżej 8 bitów na pixel = 256 kolorów (2^8),
  • EXIF (Exchangeable Image File) - wykorzystywany w aparatach fotograficznych (kompresja zgodnie ze specyfikacją JPEG, zawiera dodatkowe informacje: data zrobienia fotki, czas naświetlania itp. oraz informacje o sprzęcie),
  • JPG (JPEG - Joint Photographic Experts Group) - kompresja stratna, 24 bity na pixel (> 16 milionów kolorów), brak obsługi przeźroczystości i animacji; JPEG to schemat kompresji, a nie format plików - JPEG File Interchange Format (JFIF) jest powszechnie używanym formatem plików skompresowanym zgodnie ze schematem JPEG, JFIF używa rozszerzenia .jpg,
  • PNG (Portable Network Graphics) - ma wiele zalet formatu GIF i dostarcza kilka innych: tak jak GIF, pliki PNG są skompresowane bez straty, PNG natomiast może przechowywać kolory 8, 24 i 48 bitów na pixel i odcienie szarości: 1, 2, 4, 8 i 16 bitów na pixel (pliki GIF mogą używać tylko 1, 2, 4 lub 8 bitów na pixel). Ponadto PNG może przechowywać wartość alfa dla każdego pixela (stopień zmieszania z kolorem tła) i stopniowo wyświetlać obrazek oraz może zawierać korekcję gammy i koloru,
  • TIFF (Tag Image File Format) - elastyczny i rozszerzalny format (można dodawać nowe tagi, swobodnie określa ilość bitów na pixel i algorytm kompresji).

Abstrakcyjna klasa bazowa Image (z System.Drawing, nie mylić z Microsoft.VisualStudio.TeamSystem.Data.Generators, System.Web.UI.MobileControls, System.Web.UI.WebControls, System.Windows.Controls (WPF)) dostarcza funkcjonalności dla dziedziczących z niej klas Bitmap i Metafile. Image daje możliwość tworzenia, ładowania, modyfikacji i zapisywania obrazków takich jak .bmp, .jpg i .tif. Tworzenie instancji:

  • public static Image FromFile(string filename) - jeśli plik nie jest w poprawnym formacie lub gdy GDI+ nie obsługuje formatu pixeli pliku, to metoda rzuca OutOfMemoryException,
  • public static Image FromStream(Stream stream),
  • public static Image FromStream(Stream stream, bool useEmbeddedColorManagement, bool validateImageData).

Klasa Bitmap (System.Drawing) jest wykorzystywana dla stałych obrazków, natomiast Metafile (System.Drawing.Imaging) dla animowanych. Bitmap zawiera 2 ważne metody, których brakuje w Image:

  • public Color GetPixel(int x, int y),
  • public void SetPixel(int x, int y, Color color).

Przykład:

Bitmap animatedImage;

public Form1()
{
  InitializeComponent();

  SetStyle(ControlStyles.ResizeRedraw, true);
  this.Paint += new PaintEventHandler(Form1_Paint);

  Image img = Image.FromFile("C:\\Users\\Public\\Pictures\\Sample Pictures\\Humpback Whale.jpg");
  pictureBox1.BackgroundImage = img;
  Bitmap b = new Bitmap(@"C:\Users\Public\Pictures\Sample Pictures\Desert Landscape.jpg");
  pictureBox2.BackgroundImage = b;

  Bitmap bm = new Bitmap(200, 200);
  Graphics g = Graphics.FromImage(bm); // !
  Brush brush = new HatchBrush(HatchStyle.LargeGrid, Color.Yellow, Color.Lime);

  System.ComponentModel.TypeConverter converter = System.ComponentModel.TypeDescriptor.GetConverter(typeof(Point));
  Point point1 = (Point)converter.ConvertFromString("200; 200");
  Point point2 = point1 - new Size(200, 200);

  Rectangle rect = new Rectangle(point2, (Size)point1);
  g.FillRectangle(brush, rect);
  g.DrawIcon(SystemIcons.Question, (200 - SystemIcons.Question.Width) / 2, (200 - SystemIcons.Question.Height) / 2); // 32x32

  Bitmap icon = SystemIcons.Asterisk.ToBitmap();
  for (int i = 0; i < icon.Width; i++)
  {
    icon.SetPixel(i, i, Color.Red);
    icon.SetPixel(icon.Width - i - 1, i, Color.Red);
  }
  g.DrawImage(icon, 0, 0);
  bm.Save(@"C:\Users\Compal\Desktop\temp.gif", System.Drawing.Imaging.ImageFormat.Gif); // utrata ładnych gradientów, w przypadku jpg strata jakości

  animatedImage = new Bitmap(@"C:\Users\Compal\Desktop\st_patricks_day_animated_gif_2.gif");
  ImageAnimator.Animate(animatedImage, new EventHandler(this.OnFrameChanged));
}

private void OnFrameChanged(object o, EventArgs e)
{
  this.Invalidate(new Rectangle(0, 0, animatedImage.Width, animatedImage.Height)); // i tak widać migotanie
}

void Form1_Paint(object sender, PaintEventArgs e)
{
  GraphicsUnit gu = GraphicsUnit.Pixel;
  if (e.ClipRectangle != animatedImage.GetBounds(ref gu))
  {
    Bitmap bm = new Bitmap(@"C:\Users\Public\Pictures\Sample Pictures\Desert Landscape.jpg");
    Graphics g = this.CreateGraphics();
    g.DrawImage(bm, 0, 0, this.ClientSize.Width, this.ClientSize.Height);
  }

  ImageAnimator.UpdateFrames();
  e.Graphics.DrawImage(this.animatedImage, new Point(0, 0));
}

Ostatnią rzeczą są czcionki. Przejdę od razu do przykładu:

public Form1()
{
  InitializeComponent();
  this.Paint += new PaintEventHandler(Form1_Paint);
}

void Form1_Paint(object sender, PaintEventArgs e)
{
  Font f = new Font("Arial", 30, FontStyle.Bold);
  e.Graphics.DrawString("Hello, World!", f, Brushes.Blue, 10, 10);

  FontFamily ff = new FontFamily("Arial");
  f = new Font(ff, 12);

  FontConverter converter = new FontConverter();
  f = (Font)converter.ConvertFromString("Arial, 12pt");

  Rectangle r = new Rectangle(new Point(40, 80), new Size(80, 120));
  StringFormat f1 = new StringFormat(StringFormatFlags.NoWrap);
  StringFormat f2 = new StringFormat(f1);
  f1.LineAlignment = StringAlignment.Near;
  f1.Alignment = StringAlignment.Center;
  f1.Trimming = StringTrimming.EllipsisCharacter;
  f2.LineAlignment = StringAlignment.Center;
  f2.Alignment = StringAlignment.Far;
  f2.FormatFlags = StringFormatFlags.DirectionVertical;
  e.Graphics.DrawRectangle(Pens.Black, r);
  e.Graphics.DrawString("Format1", new Font("Courier New", 20), Brushes.Red, (RectangleF)r, f1); // wymaga obiektu Brush, a nie Pen
  e.Graphics.DrawString("Format2", this.Font, Brushes.Red, (RectangleF)r, f2);      
}

Zrzut ekranu:

fonts

Tagi:

70-536

Dodaj komentarz





  • Komentarz
  • Podgląd
Loading



Zmodyfikowany BlogEngine.NET (Bazowa szata graficzna: Mads Kristensen/Zdjęcia panoramiczne)
Hosting dzięki uprzejmości PCSS/Centrum Innowacji Microsoft
(c) Mateusz Chodyła (Panel logowania)