business

Gör GDI+ till ett minne blott – använd WPF!

Anders Tufvesson 03 mars 2012

I var och vart annat projekt man sitter i (i alla fall när det gäller e-handel eller integration) så skall det skalas bilder. De skall ha bestämda storlekar, bestämda bredder, de ska beskäras och ramas in.

GDI+ ligger under System.Drawing vilket har en liten drawback, ända upp till senaste versionen av .NET (4.5) så skriver Micosoft så här på MSDN:
Classes within the System.Drawing namespace are not supported for use within a Windows or ASP.NET service. Attempting to use these classes from within one of these application types may produce unexpected problems, such as diminished service performance and run-time exceptions. For a supported alternative, see Windows Imaging Components.
GDI+ ligger säkerligen på många av våra webbar och i många av våra Windows-tjänster och fungerar riktigt bra, men varför chansa i fortsättningen? Dessutom kan jag tycka att resultatet av det som GDUI+ genererar inte alltid är tillfredsställande ur en kvalité. Men det finns en lösning!

Windows Imaging Components

Det finns två sätt att arbeta med Windows Imaging Components (vidare kallat WIC). Det ena är via Windows Presentation Foundation (vidare kallat WPF) och det andra är via COM. Det senare alternativet kan vara användbart där vi har problem med trust eller vi inte vill programmera i något av de språk som tillhandahålls inom ramen för .NET. Men för den här gången kommer vi fokusera på C#.

Tanken jag har med detta lilla blogginlägg är att ge er alla lite hjälp på vägen, lite kod ni kan kopiera och utgå i från i era egna projekt. Så slipper ni skriva allt från början, vilket gör en övergång ganska mycket smidigare.

Jag har fokuserat på tre olika typer av bildomskalingar.

  • En omskalning där vi håller en av sidorna konstant och låter den andra skala efter behov. Det här är det klassiska sättet att skala om bilder.
  • Där vi använder en fixerad storlek (både höjd och bredd) på resultatet och lägger in vår omskalde bild i denna, så att den får plats.
  • Samt en tredje variant där vi, likt vår förra variant, använder en fixerad storlek på bilden men den här gången så skalar vi ner bilden så att en av sidorna exakt matchar en av resultatet sidor och sedan beskär vi bort överflödet av bilden.

Klassisk omskalning

Den klassisk omskalningen är bara ”vänligt” implemneterad, med det menar jag att den alltid kommer att skala om en bild proportionellt. Den väljer alltid den skalning som blir minst om man anger båda parametrarna, annars kan man skicka in 0 (noll) på en av dem för att vara övertygad om att skalning sker med den aktuella parametern i avseende.

private BitmapFrame ResizeImage(BitmapSource input, int width, int height) {
  double newHeightRatio = (height / (double)input.PixelHeight);
  double newWidthRatio = (width / (double)input.PixelWidth);

  if (newHeightRatio <= 0 && newWidthRatio > 0)
    newHeightRatio = newWidthRatio;
  else if (newWidthRatio <= 0 && newHeightRatio > 0)
    newWidthRatio = newHeightRatio;

  var scale = Math.Min(newWidthRatio, newHeightRatio);
  if (scale > 1)
    scale = 1;
  var target = new TransformedBitmap(input, new ScaleTransform(scale, scale));
  return BitmapFrame.Create(target);
}

WPF innehåller en så kallad BitmapSource som transformerar vår bild, denna tar emot en transformationstyp och .NET tillhandahåller några standardiserade. Dessa är

  • MatrixTransform
  • RotateTransform
  • ScaleTransform
  • SkewTransform
  • TranslateTransform

Just nu kommer jag enbart att fokusera på ScaleTransform eftersom det är den som är mest intressant för mig, men här kan vi lätt modifiera koden ovan för att exempelvis rotera eller skeva en bild istället.

Det här sättet att skala bilder med WPF är det snabbaste och ger en helt godkänd bildkvalité. Vi har inte heller någon möjlighet att direkt påverka bilddjupet, resultatbilden kommer i detta fall att ha samma upplösning (DPI) som orginalet.

Det här är ressultatet av en sådan här omskalning. Jag har talat om att bilden skall vara 200 px hög.

Det inte den här metoden ger möjlighet till är att välja vilken algoritm som skall användas för omskalningen. För att kunna ange detta måste vi använda ett annat sätt när vi skalar om bilderna, och förlorar då effektivitet. Det här är en avvägning man får göra i fall till fall, dock tycker jag att den algoritm som används av exemplet ovan ger en bra bildkvalité till en bra hastighet.

public BitmapFrame Resize(BitmapFrame input, int width, int height, BitmapScalingMode salingMode, int dpi) {
  var group = new DrawingGroup();
  RenderOptions.SetBitmapScalingMode(group, scalingMode);
  group.Children.Add(new ImageDrawing(photo, new Rect(0, 0, width, height)));
  var targetVisual = new DrawingVisual();
  var targetContext = targetVisual.RenderOpen();
  targetContext.DrawDrawing(group);
  var target = new RenderTargetBitmap(width, height, dpi, dpi, PixelFormats.Default);
  targetContext.Close();
  target.Render(targetVisual);
  var targetFrame = BitmapFrame.Create(target);
  return targetFrame;
}

I exemplet ovan kan man se hur vi kan välja både skalningsmetod och djup i bilden som vi producerar. Vi använder en helt annan metodik för att skala bilderna vilken tar mer kraft från systemet.

Skala en bild och placera den i en ram

Om man alltid vill ha en fast storlek på bilderna, tillexempel till en produklista där det skulle se otroligt dumt ut om bilderna var i olika bredder eller höjder, kan man välja att lägga dem i en ”omgivande ram”. Lösningen är vanlig och gör det mycket enklare att designa webbplatser.

public BitmapFrame ResizeAndFit(BitmapSource input, int width, int height) {
  if (input.PixelWidth == width && input.PixelHeight == height)
    return BitmapFrame.Create(input);

  if (input.Format != PixelFormats.Bgra32 || input.Format != PixelFormats.Pbgra32)
  input = new FormatConvertedBitmap(input, PixelFormats.Bgra32, null, 0);

  var scale = Math.Min((double)width / input.PixelWidth, height / (double)input.PixelHeight);
  if (scale > 1)
    scale = 1;

  var x = (int)Math.Round((width - (input.PixelWidth * scale)) / 2);
  var y = (int)Math.Round((height - (input.PixelHeight * scale)) / 2);
  var scaled = new TransformedBitmap(input, new ScaleTransform(scale, scale));
  var stride = scaled.PixelWidth * (scaled.Format.BitsPerPixel / 8);
  var result = new WriteableBitmap(width, height, scaled.DpiX, scaled.DpiY, scaled.Format, null);
  var colorFrame = GetColorBitmap(width, height, Brushes.White);
  var colorFrameStride = colorFrame.PixelWidth * (colorFrame.Format.BitsPerPixel / 8);
  var colorFrameData = new byte[colorFrame.PixelHeight * colorFrameStride];
  var data = new byte[scaled.PixelHeight * stride];

  colorFrame.CopyPixels(colorFrameData, colorFrameStride, 0);
  scaled.CopyPixels(data, stride, 0);

  result.WritePixels(new Int32Rect(0, 0, width, height), colorFrameData, colorFrameStride, 0, 0);
  result.WritePixels(new Int32Rect(0, 0, scaled.PixelWidth, scaled.PixelHeight), data, stride, x, y);

  return BitmapFrame.Create(result);
}

private BitmapSource GetColorBitmap(int width, int height, Brush color) {
  var renderBitmap = new RenderTargetBitmap(size.Width, size.Height, 96, 96, PixelFormats.Pbgra32);
  var drawingVisual = new DrawingVisual();
  using (var context = drawingVisual.RenderOpen()) {
    context.DrawRectangle(color, null, new Rect(0, 0, size.Width, size.Height));
  }
  renderBitmap.Render(drawingVisual);
  return renderBitmap;
}

Den här typen av omskalning blir lite mer avancerad. Här måste vi nämligen rita ut vår omskalde bild i en ytterligare bild. Först så ser vi till att båda våra bildformat är på samma format, om vår inkommande bild inte är 32bitars RGB (Bgra32) så omvandlar vi källan till detta, exempel på andra format kan vara 16bitars RGB eller CMYK.

Efter detta skalar vi ner bilden så att den får plats i vår ram. Det vi gör är att vi räknar ut proportionell skalning i både X- och Y-led för att sedan välja den som är minst. Det kommer vara den skalfaktor vi använder för att se till att bilden får plats i vår destinationsstorlek.

Som standard är vår nya skrivbara bitmap transparent, detta fungerar mycket bra för PNG och kanske till och med är önskvärt. Det vi i så fall gör är att vi raderar våran colorFrame från koden ovan och låter inte denna skrivas till bilden. När vi skalar foton är det i de flesta fall önskvärt att spara bilderna som JPEG eftersom det tar minst plats. JPEG stödjer inte transparens och därför måste vi måla vår bakgrund i en önskad färg.

Vi kopierar sedan över vår bakgrund och därefter vår bild till vår destination och returnerar denna. Omskalningen är klar!

Det här är resultatet. Jag har talat om att jag vill ha en bild som är 500 px bred och 200 px hög, och med blå bakgrund.

Skala en bild och beskär det som inte får plats

Om du vill ha en fast bildstorlek men inte vill ha en ”ful” färgad ram kring din bild så kan det vara nog så bra att beskära bort en del av bilden. Om dina orginalbilder ligger väldigt nära dina omskalade bilder i proportioner så är detta ett utmärkt alternativ, på bilderna som jag använder blir resultatet dessvärre mycket sämre. s

public BitmapFrame ResizeAndCrop(BitmapSource input, int width, int height) {
  if (input.PixelWidth == width && input.PixelHeight == height)
    return BitmapFrame.Create(input);

  var useScaleH = (width / (double)input.PixelWidth) * input.PixelHeight;
  var newSizeH = 0;
  var newSizeW = width;
  if (useScaleH < height) {
    var newSizeH = height;
    var newSizeW = 0;
  }

  var scaled = ResizeImage(input, newSizeW, newSizeH);
  var x = ((scaled.PixelWidth - width) / 2);
  var y = ((scaled.PixelHeight -height) / 2);
  var stride = scaled.PixelWidth * (scaled.Format.BitsPerPixel / 8);
  var result = new WriteableBitmap(width, height, scaled.DpiX, scaled.DpiY, scaled.Format, null);
  var data = new byte[scaled.PixelHeight * stride];
  var copyWidth = scaled.PixelWidth - (scaled.PixelWidth - width);
  var copyHeight = scaled.PixelHeight - (scaled.PixelHeight - height);
  if (x < 0) {
    x = 0;
    copyWidth = scaled.PixelWidth;
  }
  if (y < 0) {
    y = 0;
    copyHeight=scaled.PixelHeight
  }

  scaled.CopyPixels(data, stride, 0);
  result.WritePixels(new Int32Rect(x, y, copyWidth, copyHeight), data, stride, 0, 0);

  return BitmapFrame.Create(result);
}

Det här ser mer avancerat ut än vad det är. Vi måste först räkna ut vilken sida som, när vi skalat ner bilden, kommer att vara lika stor som den nya bilden medan den andra sidan är större. När vi gjort det kan vi skala ner bilden proportionellt.

Vi skapar därefter en destinationsbild som är exakt så stor som vi vill ha den. Eftersom vår nedskalade bild är större än denna måste vi räkna ut en ram som skall användas för att beskära bilden. Det gör vi genom att räkna ut hur mycket som ”hänger över” i bredd- och höjdled. Vi centrerar bilden genom att dela dessa värden i hälften och koperar nu över de pixlar som ligger inom ramen till vår destinationsbild.

Viktigt att notera här är att om orginalbilden är mindre än destinationsbilden så kommer vi bara kopiera över bilden till det övre vänstra hörnet. Det är för att vi inte vill sträcka ut bilden och förstöra den. Det bästa är inte använda denna metod vid sådana bilder, utan i stället lösa det problemet på annat sätt, exempelvis genom att lägga en ram kring den.


Det här är resultatet. Jag har talat om att bilden skall vara 500 px bred och 200 px hög. Min orginalbild är stående och denna liggande, därför förlorar vi mycket av bilden. Två bildformat som ligger nära varandra i proportioner resulterar

Avkoda och koda bilder från disk/ström.

För att allt det här ska fungera behöver du också några småsaker, exempelvis en bild-decoder och en bild encoder. Självklart finn detta inbyggt och är inte direkt rocket-sience. Här kommer lite exempel:

Stream orginalImage = null; //Load stream from image-file or like.
var orginalImageDecoder = BitmapDecoder.Create(
                                    orginalImage,
                                    BitmapCreateOptions.PreservePixelFormat,
                                    BitmapCacheOption.None);
BitmapSource source = orginalImageDecoder.Frames[0];

När du skall spara dina omskalade bilder kan du göra det i sex format som Microsoft tillhandahåller. Dessa är

  • BmpBitmapEncoder
  • GifBitmapEncoder
  • JpegBitmapEncoder
  • PngBitmapEncoder
  • TiffBitmapEncoder
  • WmpBitmapEncoder

alla dessa format används på samma sätt som nedan. JpegBitmapEncoder har dessutom möjlighet att sätta kvalité för kompressionen.

Stream resizedImage = null; //Init a writable stream for the destination
BitmapFrame resizedImageFrame = null; //Frame from one of the resize algorithms above.
var targetEncoder = new JpegBitmapEncoder(){
    QualityLevel = 85;
};
targetEncoder.Frames.Add(resizedImageFrame);
targetEncoder.Save(resizedImage);

Jag hoppas att det här har väckt dina tankar lite kring att kanske använda WPF för att göra din bildmanipulering i framtiden.

Etiketter:, , , , , , , , , , , , , , ,

03 mars 2012 |

2 Kommentarer

  1. Kommentar av Bobbi den 31 mars 2017 kl: 3:01
    Bobbi skriver:

    You’re a real deep thnekir. Thanks for sharing.

  2. Kommentar av Adiana den 31 mars 2017 kl: 3:15
    Adiana skriver:

    That’s a smart answer to a diuffcilt question.

Tyck till! :)

Designa för Windows

Anders Tufvesson 01 december 2011

Något jag väldigt sällan stöter på i min yrkesroll just nu är att göra desktop-applikationer för Windows. Under alla mina studieår på universitetet hade jag inte en enda gång möjlighet att läsa någon kurs i människa datorinteraktion och i gränssnittsdesign. Jag är enormt intresserad av dessa ämnen och väldigt iver att lära mig mer.

I förra veckan fick jag ett litet miniuppdrag att skapa en enkel desktop-applikation till en av våra kunder inom finanssektorn. Idén är superenkel. Man matar in två värden, man väljer ett filnamn och man hämtar hem en fil från nätverket. Användarna av den här applikationen är personer med varierande datorkunskaper men med en sådan här enkel applikation kan man inte göra fel, eller?

Jag tänkte dela med mig lite av några do’s and don’ts när det gäller gränssnittsdesign av desktop-applikationer. Kanske kan det inspirera er att se på gränssnitten på ett annat sätt och även kanske bygga gränssnitt annorlunda. Först och främst vill jag göra det klart att det jag skriver här inte passar i alla sammanhang – det finns alltid undantag. Dessutom är jag väldigt allergisk mot att lägga in saker i en applikation ”för att det går”.

Om man designar en applikation för ett specifikt operativsystem (exemeplvis Windows®, MacOS® eller en specifik fönsterhanterare till Linux) så ska man följa de guidelines som är uppsatta för detsamma. Genom att göra gränssnittet harmoniserat med övriga element i systemet blir ovana användare tryggare och dessutom erhålls en känsla av att applikationen hänger ihop med systemet. Vilket är väldigt viktig användarupplevelse enligt mig.

Läsordning

Det finns många studier på hur användare tittar på en ny skärm de inte sett förut. En kunskap om detta underlättar oerhört när man gör ett nytt gränssnitt. Bilden nedan visar hur användaren tittar från vänster till höger och uppifrån och ner. I länder med annan läsordning än vänster till höger kan detta variera.

Bild: Microsoft Corporation

På grund av att den vanlige användaren tittar så här ska vi också fylla på med information från vänster till höger. Den viktigaste informationen till vänster och mindre viktig information till höger. Jobbar vi med formulär ska dessa fyllas i från vänster till höger i första hand och i andra hand uppifrån och ner.

Jag ska visa en bild som jag tycker är så givande. Jag har lånat den från Microsoft Guidelines för gränssnittsdesign. På bilden ser vi ett gränssnitt där ordningen man ska utföra uppgifterna är väldigt i oordning. Det är jobbigt för en användare att arbeta i det här formuläret eftersom man först och främst inte direkt ser i vilken ordning det krävs att man gör saker men också för att det tar onödigt med tid att hoppa mellan de olika fälten.

Bild: Microsoft Corporation

Om vi ordnar om fälten i det här formuläret enligt läsordningen ovan så blir det som bilden nedan. Direkt ser man i vilken ordning man förväntas fylla i formuläret. Det här är ganska enkelt och tänka på och man behöver inte ha några layout-kunskaper eller någon känsla för layout för att lyckas. Det är bara rena regler! Det gillar jag!

Bild: Microsoft Corporation

Ordningen vänster till häger gäller också när man bygger verktygsfält. Fyll på med de mest använda ikonerna till vänster och fortsätt sedan åt höger med fallande signifikans och användning. Tänk också på att skärmbredden (i pixlar) varierar för varje användare vilket gör att för vissa användare kan knappar i höger-kanten döljas när fönstret blir mindre. Detta bör man lösa genom att låta de knappar som döljs kunna visas i en nedfällbar meny, det är oftast inte rekommenderat att skapa en ytterligare rad eftersom detta bryter tanken om att de mest använda verktygen skall ligga till vänster.

Bild: Apple Inc.

Alignering (aligning)

En viktig sak när man arbetar med gränssnittsdesign är att hålla designen så alignerad  som möjligt. Detta hjälper ögat att sålla och sortera i informationen. Tittar du på bilden kanske du instiktivt känner att det är svårt att hitta informationen i fönstret. Ju fler kolumner (grids) vi lägger till desto mer avancerat blir gränssnittet.

Bild: Microsoft Corporation

I nästa bild reduceras antalet kolumner till 4 i stället för 9. Gränssnittet känns ”luftigare” och det är lättare för ögat att hitta informationen vi är ute efter. ”Less is more” skulle man kunna säga här. Det här är också en enkel regel man alltid kan applicera, och gäller även på webben faktiskt.

Bild: Microsoft Corporation

Menyer och menyval

Något annat som är ganska enkelt att arbeta med är segment i menyer. Här finns ingen direkt regel, men kommandon som logiskt hör i hop grupperas ihop. Exempel: Nytt, Spara, Öppna, Stäng hör ihop, och Klipp ut, Kopiera, Klistra in hör ihop. Genom att segmentera menyn så blir det flera fall enklare att snabbt hitta det man söker.

Bild: Apple Inc.

Det är högt rekommenderat att arbeta med kortkommandon. Bilden ovan visar en menyer på MacOS® men principen är den samma även i Windows®. Det vi bör tänka på när vi tilldelar kortkomandon är att man gör det enligt ”gängse standard”. Det finns en visst antal kortkommandon som användare vanligen vill utför samma operationer i olika program. Detta är exempelvis; kopiera (CTRL+C), Spara (CTRL+S), Avsluta (ALT+F4), Öppna (CTRL+O). Om ditt program exempelvis använder CTRL+S för att stänga programmet utan att spara pågående arbete blir användaren ganska förvånad (och kanske irriterad) när han använder det kommandot och förväntar sig att det skall spara hans arbete. Dock är detta en helt annan historia, som ni kan läsa mer om på Microsofts webbplats!

Inaktivera otillgänliga val

En viktig detalj i ett gränssnitt är att enbart visualisera de verktyg som fungerar att använda. Ett formulär kan ha många kontroller som kanske kan användas vid ett visst givet tillfälle.

Det enklaste, och kanske bästa sättet, att göra detta är att inaktivera de kontroller som inte är tillgängliga. Det gör att användare inte försöker använda dem eller lägger energi på att fylla i dem.

Nästling

Jobba gärna med att nästla element som hör i hop, exempelvis genom att lägga alla kontroller för att söka i en gruppering vid namn ”Sök”. Det gör det lättare för användaren att hitta vad han eller hon söker. Det är också bra att indentera element som är underelement till en annan kontroll, som enbart är tillgängliga när ett överordnat element är aktiverat.

Bild: Microsoft Corporation

Det man dock aldrig ska göra är att nästla simpla element. Med det menar jag att man exempelvis aldrig ska lägga ett textinmatningsfält i beskrivningstexten till en kryssruta. Personer med nedsatt syn och som använder skärmläsningshjälpmedel kan ha svårt att uppfatta sådana konstruktioner.

Bild: Microsoft Corporation

Försök i stället att omformulera meningen så att du kan lägga textinmatningen sist eller på en ny rad.

Spacing

Vi har ofta problem med språkanpassning (localization). Ord och meningar har olika längd på olika språk och ibland får vi inte plats med etiketten innan själva datan börjar.

Ett enkelt knep för att komma förbi detta är att ta för vana att alltid lägga den beskrivande etiketten ovanför dataetiketten. Då har vår beskrivning möjlighet att växa sig mycket större innan den inkräktar på något.  Det finns också studier (enligt ”Jävlaskitsystem”) som visar på att det är lättare för ögat att hitta den information man söker när etiketterna är ordnade på detta sätt.

Bild: Microsoft Corporation

Till slut kan jag tipsa om att alltid använda de spacing-guidlines som Microsoft Visual Studio® föreslår när du placerar ut kontroller. Det hjälper dig till att bli en bättre UI-designer för Windows. Dessutom är den så smart att den hjälper dig att hålla dina aligneringar. Så om bara du gör alla knappar och inmatningsfält som hör till samma gruppering lika stora så kommer Visual Studio ® hjälpa dig med resten!

Mer?

Är du nyfiken på mer? Då kanske du kan läsa några av de här:
http://msdn.microsoft.com/en-us/library/windows/desktop/aa511275.aspx
http://developer.apple.com/library/mac/#documentation/UserExperience/Conceptual/AppleHIGuidelines/Windows/Windows.html

http://javlaskitsystem.se/

Jag har lånat de flesta bilder från Microsoft Corporation och några från Apple Inc. för denna artikel. Om det är så att Microsoft eller Apple anser att jag använt deras material olovligt kommer jag omedelbart att ta ned dem. Jag drog mig för att göra egna bilder eftersom jag tycker bilderna är så talande och bra gjorda.

Etiketter:, , , , ,

01 december 2011 |

1 kommentar

  1. Kommentar av Carita G den 02 juni 2016 kl: 8:54
    Carita G skriver:

    Tack för denna artikel, väldigt bra infomation om hur man skapar en desktop applikation

Tyck till! :)