基礎架構
點陣圖與像素位元
Charles Petzold

目錄
Windows® Presentation Foundation (WPF) 的保留模式圖形系統,顛覆了 Windows 圖形程式設計。程式不再需要在系統要求時重新建立畫面上的視覺外觀。複合系統會保留所有圖形,並將這些圖形組合成整體的視覺外觀。
保留模式圖形的確可以讓工作更輕鬆,但是輕鬆向來不是 Windows 程式設計師追求的第一目標。真正重要的是,保留模式圖形系統與通知機制 (例如相依性屬性) 相結合之後,能夠釋放出 WPF 的彈性與威力。路徑及筆刷等圖形物件在複合系統中似乎可以保持「活躍」,而且可持續回應屬性的變更和圖形的轉換,讓這些物件可成為資料繫結與動畫的目標。
我最近發現 WPF 點陣圖具有類似的動態特質。繪製的點陣圖可持續回應變更,不僅是回應圖形的轉換 (這點我們都已經曉得),同時也能回應點陣圖內實際像素的變更。
展現這種動態回應的兩個點陣圖類別,分別是 RenderTargetBitmap 和 WriteableBitmap,它們是從 BitmapSource (WPF 中所有點陣圖支援基礎的抽象類別) 衍生的九個類別中的兩個點陣圖類別。無論程式如何使用這些點陣圖物件,無論是使用 Image 項目顯示、使用 ImageBrush 類別成為並排筆刷,或使用 ImageDrawing 類別當做大型繪圖的一部分 (也許與向量圖形混用),點陣圖絕對不會繪製完了就被遺忘。點陣圖在視覺複合系統中會保持活躍,並持續回應應用程式的變更。
使用 RenderTargetBitmap
在 RenderTargetBitmap 這個點陣圖上,您可以將型別為 Visual 的物件傳送到表面來進行繪圖。建立型別為 RenderTargetBitmap 的新物件的唯一方法就是使用建構函式,建構函式需要點陣圖的像素尺寸、以每英吋的像素數目 (DPI) 為單位的水平和垂直解析度,以及型別為 PixelFormat 的物件。
我待會兒會進一步說明 PixelFormat 的結構和相關的靜態 PixelFormats 類別。為了建立型別為 RenderTargetBitmap 的物件,您必須使用 PixelFormats.Default 或 PixelFormats.Pbgra32 來當做 RenderTargetBitmap 建構函式的最後一個引數。無論使用何者,都會建立每像素 32 個位元和透明的點陣圖。
一開始,RenderTargetBitmap 物件是完全透明的。接著您可以使用型別為 Visual 的物件來呼叫 Render 方法,以繪製點陣圖 (包括衍生自 Visual 的類別,例如 FrameworkElement 及 Control)。您可以呼叫 Clear 來還原完全透明的影像。如果目前正在顯示點陣圖,這些呼叫會立即反映在顯示的點陣圖中。
[圖 1] 顯示一個完整的小程式,用來展示 RenderTargetBitmap。這個程式會建立 1,200 個像素寬乘以 900 個像素高的點陣圖。這些值會構成點陣圖物件的 PixelWidth 和 PixelHeight 屬性。每個像素都是 4 個位元組寬,因此點陣圖會佔用 4 MB 以上的記憶體。

圖 1 RenderTargetBitmapDemo
class RenderTargetBitmapDemo : Window
{
RenderTargetBitmap bitmap;
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new RenderTargetBitmapDemo());
}
public RenderTargetBitmapDemo()
{
Title = "RenderTargetBitmap Demo";
SizeToContent = SizeToContent.WidthAndHeight;
ResizeMode = ResizeMode.CanMinimize;
// Create RenderTargetBitmap
bitmap = new RenderTargetBitmap(1200, 900, 300, 300,
PixelFormats.Default);
// Create Image for bitmap
Image img = new Image();
img.Stretch = Stretch.None;
img.Source = bitmap;
Content = img;
}
protected override void OnMouseDown(MouseButtonEventArgs args)
{
Point ptMouse = args.GetPosition(this);
Random rand = new Random();
Brush brush = new SolidColorBrush(
Color.FromRgb((byte)rand.Next(256), (byte)rand.Next(256),
(byte)rand.Next(256)));
DrawingVisual vis = new DrawingVisual();
DrawingContext dc = vis.RenderOpen();
dc.DrawEllipse(brush, null, ptMouse, 12, 12);
dc.Close();
bitmap.Render(vis);
}
protected override void OnClosed(EventArgs args)
{
PngBitmapEncoder enc = new PngBitmapEncoder();
enc.Frames.Add(BitmapFrame.Create(bitmap));
FileStream stream = new FileStream("RenderTargetBitmapDemo.png",
FileMode.Create, FileAccess.Write);
enc.Save(stream);
stream.Close();
}
}
對 RenderTargetBitmap 建構函式的呼叫,也會指定 300 DPI 的解析度。像素尺寸與解析度的組合,會產生 4 英吋寬乘 3 英吋高的點陣圖。與裝置無關的 WPF 座標系統是每一英吋 96 個單位,因此點陣圖的與裝置無關座標,是 384 個單位的寬度和 288 個單位的高度。當您檢查 BitmapSource 定義的 Width 和 Height 屬性時,就會看到這些數值。
RenderTargetBitmapDemo 程式會使用 Image 元素,來顯示未延伸的點陣圖。它也會設陷 MouseDown 事件。每次按一下滑鼠時,程式都會建立 DrawingVisual 物件,這個物件是直徑 ¼ 英吋的已填滿小圓形,然後還會呼叫 Render 方法來將它加入點陣圖影像。此圓形接著就會出現在顯示的點陣圖中。您可以將這個程式想成是在單一點陣圖中,結合繪圖與儲存功能的簡單繪製應用程式。
您可能知道 (或可以猜得到),Image 元素會藉由呼叫在 OnRender 方法期間傳遞給它的 DrawingContext 物件的 DrawImage 方法,來顯示點陣圖。當 RenderTargetBitmapDemo 程式變更點陣圖時,Image 類別不會收到 OnRender 方法的重複呼叫。在視覺構成系統中,點陣圖的這些變更會發生在更深層。
當程式終止時,RenderTargetBitmapDemo 程式會以 PNG 檔案格式儲存構成的點陣圖。查看從 RenderTargetBitmapDemo 儲存的點陣圖,您就會發現它的大小是 1,200 乘 900 像素,且每個圓形的直徑都是 75 像素,也就是每一英吋 300 DPI 為單位的 ¼ 英吋。
使用 WriteableBitmap
WriteableBitmap 類別有兩個建構函式,其中一個與 RenderTargetBitmap 建構函式很相似。前四個引數是點陣圖的像素尺寸與解析度 DPI。第五個引數是 PixelFormat 物件,但是這擁有比 RenderTargetBitmap 更多的彈性。WriteableBitmap 建構函式有一個額外的參數,可為需要的點陣圖格式提供調色盤。
WriteableBitmap 的像素全都會初始化成零。它所代表的意義端視像素格式而定。在大部分的情況下,點陣圖是全黑的。如果點陣圖支援透明度,那麼點陣圖就是透明的。如果點陣圖具有調色盤,則整個點陣圖會以調色盤中的第一個色彩著色。
變更 WriteableBitmap 與變更 RenderTargetBitmap 非常不同。針對 WriteableBitmap,您必須呼叫名為 WritePixels 的方法,此方法會從本機陣列複製實際的像素位元到點陣圖中。當然,陣列資料的格式與大小,必須與點陣圖的尺寸與像素格式相符。
首先我們來看一下比較簡單的範例。[圖 2] 顯示的 AnimatedBitmapBrush.cs 程式,會建立 WriteableBitmap,然後使用它當做並排 ImageBrush 的基礎,並將此 ImageBrush 設為視窗的 Background 屬性。程式接著會將計時器設為 100 毫秒,並重複呼叫 WritePixels 以變更點陣圖。

圖 2 AnimatedBitmapBrush
class AnimatedBitmapBrush : Window
{
const int COLS = 48;
const int ROWS = 48;
WriteableBitmap bitmap;
byte[] pixels = new byte[COLS * ROWS];
byte pixelLevel = 0x00;
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new AnimatedBitmapBrush());
}
public AnimatedBitmapBrush()
{
Title = "Animated Bitmap Brush";
Width = Height = 300;
bitmap = new WriteableBitmap(COLS, ROWS, 96, 96,
PixelFormats.Gray8, null);
ImageBrush brush = new ImageBrush(bitmap);
brush.TileMode = TileMode.Tile;
brush.Viewport = new Rect(0, 0, COLS, ROWS);
brush.ViewportUnits = BrushMappingMode.Absolute;
Background = brush;
DispatcherTimer tmr = new DispatcherTimer();
tmr.Interval = TimeSpan.FromMilliseconds(100);
tmr.Tick += TimerOnTick;
tmr.Start();
}
void TimerOnTick(object sender, EventArgs args)
{
for (int row = 0; row < ROWS; row++)
for (int col = 0; col < COLS; col++)
{
int index = row * COLS + col;
double distanceFromCenter =
2 * Math.Max(Math.Abs(row - ROWS / 2.0) / ROWS,
Math.Abs(col - COLS / 2.0) / COLS);
pixels[index] =
(byte)(0x80 * (1 + distanceFromCenter * pixelLevel));
}
bitmap.WritePixels(new Int32Rect(0, 0, COLS,ROWS),pixels,COLS,0);
pixelLevel++;
}
}
常數 COLS 和 ROWS 值會定義此點陣圖的像素尺寸。這些值可用於並排筆刷 Viewport 矩形,而且也包含在計時器的 Tick 事件處理常式中。像素格式會設為 PixelFormats.Gray8,表示點陣圖中的每個像素都會以 8 個位元的值 (代表灰色陰影) 來表示,像素值 0x00 是黑色,0xFF 是白色。WriteableBitmap 建構函式的最後一個引數會設為 Null,因為 Gray8 格式不需要調色盤。
由於每個像素都是 1 個位元組,我稱為 pixels 的位元組陣列的尺寸,會直接使用 COLS × ROWS 來計算。該 pixels 陣列會定義為欄位,才不必在每次呼叫 Tick 事件處理常式時重新建立。陣列中的資料必須從左到右以最頂端的列開始,接下來是第二行...並依此類推。Tick 事件處理常式包含點陣圖列與欄的兩個迴圈,但是它會將這兩個值合併成一維的陣列索引:
int index = row * COLS + col;
WritePixels 不會接受多維陣列。WritePixels 的第一個引數是以像素座標為單位的 Int32Rect 結構,藉此指出要更新的點陣圖矩形子集。Int32Rect 物件的 X 及 Y 屬性會描述相對於點陣圖左上角的矩形左上角座標;Width 和 Height 屬性則會指定此矩形的像素尺寸。若要更新整個點陣圖,請將 X 及 Y 設為 0,並將 Width 和 Height 設為點陣圖 PixelWidth 和 PixelHeight 屬性。我稍後會討論 WritePixels 的最後兩個引數。
我必須承認,我原本打算為此點陣圖編撰稍微不同的動畫模式,不過我在此摸索出來的成品似乎蠻有趣的,至少有小部分是如此。其中一個影像顯示在 [圖 3] 中。
圖 3 AnimatedBitmapBrush 顯示
像素陣列
當點陣圖並非採用每個像素一個位元組的格式,而且只更新點陣圖的矩形子集時,呼叫 WritePixels 的方式就可能會比較複雜。學習這項技巧很重要,因為相同的像素位元陣列格式,會在靜態 BitmapSource.Create 方法中用來建立新的點陣圖,而在 BitmapSource 的 CopyPixels 方法中也必須用它來複製點陣圖的像素位元到陣列。在這三種方法中,您都可以選擇性使用 IntPtr 來指向本機緩衝區,不過我要把重點放在陣列方法上。
點陣圖中的像素總數,是由 BitmapSource 類別定義的 PixelWidth 與 PixelHeight 屬性的乘積。BitmapSource 也定義了型別為 PixelFormat 的 Get 專用 Format 屬性,這個屬性本身會定義名為 BitsPerPixel 的 Get 專用屬性 (範圍從 1 到 128)。在一個極端,單一位元組會儲存 8 個連續像素的資料;但在另一個極端,每個像素會需要 16 個位元組的資料。您可能只會針對像素位元使用 byte、ushort、uint 或 float 的陣列。
您提供給 WritePixels 方法的 Int32Rect 物件,會定義點陣圖內的矩形子集。像素陣列的位元組數目必須包含足夠的列數及欄數資料,這會由 Int32Rect 物件指定。此情況會比較複雜,因為多個像素格式會將多重像素儲存在單一位元組中。對於這些格式,每個資料列都必須以位元組界限開始。
舉例來說,假設您使用的點陣圖包含每像素 4 個位元的格式。而您所存取或更新的點陣圖矩形區域,是 5 個像素寬乘以 12 個像素高。這些是您提供的 Int32Rect 物件的 Width 及 Height 屬性。陣列中的第一個位元組會包含前兩個像素的資料,第二個位元組則包含接下來兩個像素的資料,但是第三個位元組只會在第一列中包含第五個像素的資料。下一個位元組會對應到第二列的前兩個像素。
每一資料列都需要 3 個位元組,整個矩形區域需要 36 個位元組。為了協助進行計算,WritePixels 方法需要一個稱為 stride 的引數。這是指每個像素資料列的位元組數目。stride 的一般計算方式如下:
int stride = (width * bitsPerPixel + 7) / 8;
寬度等於 Int32Rect 結構的 Width 屬性。即使您使用 ushort、uint 或 float 值的陣列,stride 值永遠都會以位元組為單位。接著您可以計算陣列中的位元組總數,如下所示:
int dimension = height * stride;
若您使用 ushort、uint 或 float,請分別除以 2、4 或 8。
您可能還記得 Windows API 會要求點陣圖資料的每一列都以 32 位元記憶體界限開始,因此 stride 必須是四的倍數。這點在 WPF 中不是必要的。不過,您可以將 stride 設定得比公式計算出來的值更大 (如果這麼做比較方便的話)。比方說,您可能使用每像素一個位元組的點陣圖,但是您的陣列型別是 uint 而非 byte。在這種情況下,陣列中的每個項目都會儲存四個像素。即使沒有強制要求,還是建議您要在單位界限上開始陣列的每一列,因為在許多現行的硬體平台上,對齊的複本通常會比未對齊的複本執行得更快。
點陣圖像素格式
點陣圖中的每個像素,都會以定義該點陣圖色彩的一個或多個位元表示。在 WPF 中,特定的像素格式是以結構型別為 PixelFormat 的物件來表示。靜態 PixelFormats 類別會定義型別為 PixelFormat 的 26 個靜態屬性,在建立點陣圖時您可使用這些屬性。它們分成兩大群組,如 [圖 4] 所示:可寫入格式和不可寫入格式。除了三個例外 (Bgr555、Bgr565 及 Bgr101010),屬性名稱中的任何數字都會與每像素的位元數目一樣。

圖 4 PixelFormat 類別的靜態屬性
| 可寫入格式 |
| Indexed1 |
| Indexed2 |
| Indexed4 |
| Indexed8 |
| |
| BlackWhite |
| Gray2 |
| Gray4 |
| Gray8 |
| |
| Bgr555 |
| Bgr565 |
| |
| Bgr32 |
| Bgra32 |
| Pbgra32 |
| 不可寫入格式 |
| Default |
| |
| Bgr24 |
| Rgb24 |
| Bgr101010 |
| Cmyk32 |
| |
| Gray16 |
| Rgb48 |
| Rgba64 |
| Prgba64 |
| |
| Gray32Float |
| Rgb128Float |
| Rgba128Float |
| Prgba128Float |
使用 BitmapSource.Create 建立點陣圖時,您可以使用 PixelFormats 類別的任一靜態屬性 (PixelFormats.Default 除外)。建立型別為 WriteableBitmap 的點陣圖時,您只能使用可寫入格式。
以 Indexed 開頭的格式需要靜態 BitmapSource.Create 方法中的 ColorPalette 物件或 WriteableBitmap 建構函式。每個像素都是 ColorPalette 物件中的索引,因此這四個格式會分別與最大值為 2、4、16 及 256 個色彩關聯。如果實際像素位元沒有達到上限,那麼 ColorPalette 不一定要是色彩數目的最大值。
針對低於每像素 8 個位元的格式,位元組中的最大顯著性的位元會對應到最左邊的像素。以 Indexed2 格式為例,0xC9 的位元組等於二進位的 11001001,且對應到 11、00、10 及 01 的四個 2 位元值。而這些值又會對應到 ColorPalette 集合中第四個、第一個、第三個及第二個色彩。
BlackWhite、Gray2、Gray4 及 Gray8 格式,都是包含每像素 1、2、4 或 8 個位元的灰色陰影點陣圖。都是零 (0) 的像素是黑色,而都是一 (1) 的像素是白色。
其餘的五個可寫入格式都是色彩格式。字母 B、G 及 R 分別代表藍色、綠色及紅色等原色。字母 A 代表 Alpha 色頻,並且會指出點陣圖支援透明度。字母 P 代表預乘 Alpha,我稍後會加以討論。
Bgr555 和 Bgr565 格式都需要每像素 16 個位元 (或 2 個位元組)。Bgr555 格式會針對每個原色使用 5 個位元 (因此可允許 32 個漸層),並留下一個位元。如果藍色原色的位元是以 B0 (最小顯著性的位元) 到 B4 (最大顯著性的位元) 來表示,而綠色和紅色也是如此,那麼兩個連續的資料位元組就可以儲存三個原色,如 [圖 5] 所示。
圖 5 雙位元組像素格式 (按一下影像以放大圖片)
請注意,綠色位元是分散到 2 個位元組上。如果您了解像素其實是 16 位元的無正負號之整數,且先儲存最小顯著性的位元組,那麼就更容易理解這種排列方式。[圖 6] 中的圖表顯示原色如何編碼成單一的短整數 (Short Integer)。
圖 6 短整數 (Short Integer) 像素格式 (按一下影像以放大圖片)
如此一來您便可以確信 [圖 7] 中顯示的 Gradient555Demo 程式會建立此格式的點陣圖,並寫入像素以便顯示從左至右的藍色到綠色漸層。請注意,ushort 陣列只是列和欄總數的乘積。

圖 7 Gradient555Demo
class Indexed2Demo : Window
{
const int COLS = 50;
const int ROWS = 20;
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new Indexed2Demo());
}
public Indexed2Demo()
{
Title = "Bgr555 Bitmap Demo";
WriteableBitmap bitmap = new WriteableBitmap(COLS, ROWS, 96, 96,
PixelFormats.Bgr555, null);
ushort[] pixels = new ushort[ROWS * COLS];
for (int row = 0; row < ROWS; row++)
for (int col = 0; col < COLS; col++)
{
int index = row * COLS + col;
int blue = (COLS - col) * 0x1F / COLS;
int green = col * 0x1F / COLS;
ushort pixel = (ushort)(green << 5 | blue);
pixels[index] = pixel;
}
int stride = (COLS * bitmap.Format.BitsPerPixel + 7) / 8;
bitmap.WritePixels(new Int32Rect(0, 0, COLS, ROWS), pixels,
stride, 0);
Image img = new Image();
img.Source = bitmap;
Content = img;
}
}
這段程式碼示範使用型別並非 byte 的陣列,且陣列對應到每像素之位元組數目的好處。針對任何列和欄像素位址,陣列索引只是欄加上列與每欄像素數目的乘積。
Bgr565 格式非常類似 Bgr555,只不過它的綠色 (眼睛感受最敏銳的色彩) 使用的是 6 個位元。其餘三個可寫入格式處理起來簡單多了。它們都使用每像素 4 個位元組,以藍色開始。在 Bgr32 格式中,最後四個位元組是零 (0);其中沒有透明度。在其他兩個格式中,第四個位元組是 Alpha 色頻。Alpha 值的範圍包括從透明的 0x00 到不透明的 0xFF。將像素當做無正負號的 32 位元整數時,最小顯著性的 8 個位元會用於藍色的編碼,而最大顯著性的 8 位元會是零 (0) 或 Alpha 色頻。
一般而言,點陣圖中的位元組順序會對應到屬性名稱中的字母 B、G、R 及 A 的順序。不可寫入格式的清單,會以只使用每像素 16 或 24 個位元的多個色彩格式開始。Bgr101010 格式會使用每像素 32 個位元,但每個原色則是 10 個位元。以 32 位元無正負號之整數來表示像素時,最小顯著性的 10 個位元會用於藍色。Cmyk32 格式會為列印作業所用的青色、洋紅色、黃色及黑色程度編碼。
Gray16、Rgb48、Rgba64 及 Prgba64 格式則會為 16 位元灰色陰影和 16 位元原色編碼。如果您使用的硬體和應用程式 (像是醫學影像處理) 需要更高的色彩精準度,您現在將可以儲存和顯示高解析度的點陣圖資料。不過,在其他情況下,就沒有必要使用這些格式。在每原色 8 個位元的色彩顯示中,或是儲存成每原色 8 個位元的檔案格式時,都會忽略額外的色彩精準度。
像素格式清單最後還有四個格式,這些會使用單精度浮點值來表示色階和透明度。這些格式是以 scRGB 色彩空間和 Gamma 數值 1 為基礎,而非慣例的 sRGB 色彩空間和 Gamma 值 2.2 (如需相關說明,請參閱我的著作《Applications = Code + Markup》中的第 24-25 頁)。浮點色彩值 0.0 會對應到位元組值 0x00 (黑色),而浮點色彩值 1.0 則會對應到位元組值 0xFF,但是如果顯示裝置的色域圖比視訊顯示器更廣的話,浮點色彩值就可能會超過 1。
PixelFormats Errata
在 PixelFormats 文件中,關於某些格式的說明似乎令人混淆。Gray16、Rgb48、Rgba64 及 Prgba64 格式都記載為以 Gamma 值 1 為基礎,但是除了 Gray16 以外,這些格式卻又矛盾地記載為 sRGB 格式。事實不然。只有 Float 像素格式使用 scRGB 色彩格式和 Gamma 值 1。
您可能想要一個通用方法,來將像素打散成色彩元件,或從色彩元件建構像素。PixelFormat 結構包含名為 Mask 的屬性,這是型別為 PixelFormatChannelMask 之物件的集合。每個色頻都有一個 PixelFormatChannelMask 物件,順序是藍色、綠色、紅色及 Alpha。
PixelFormatChannelMask 結構會定義 Mask 屬性 (這是位元組的集合),這個屬性的數字會等於每像素的位元組數目,而且會對應到像素的位元組順序。舉例來說,Bgr555 格式有三個 PixelFormatChannelMask 物件,分別具有 2 個位元組。以藍色來說,這 2 個位元組是 0x1F 和 0x00;綠色是 0xE0 和 0x03;而紅色則是 0x00 和 0x7C。若要使用此資料,您必須衍生自己的位元移位係數。
我說過,您可以使用任何 PixelFormats 成員 (BitmapSource.Create 方法的 PixelFormats.Default 除外),但僅限 WriteableBitmap 建構函式的可寫入格式,如 [圖 4] 所示。若您查看 WriteableBitmap 文件,就會發現有一個替代解構函式,可以建立來自任何 BitmapSource 物件的 WriteableBitmap 物件。
當然,您可以先從 [圖 4] 的不可寫入格式建立 BitmapSource 物件,然後再根據該 BitmapSource 來建立 WriteableBitmap。但是如此也無法避過這項限制:任何包含不可寫入格式的點陣圖都會轉換成 Bgr32 或 Pbgra32 格式,端視 Alpha 色頻是否存在而定。
您可以使用支援的檔案格式將所建立的點陣圖儲存到檔案,包括 BMP、GIF、PNG、JPEG、TIFF 及 Microsoft Windows Media Photo 等檔案格式。不過,點陣圖資料可能會在過程中轉換成不同的格式。例如,在儲存成 GIF 檔案時,點陣圖一定會先轉換成 Indexed8 格式。在儲存成 JPEG 檔案時,點陣圖一定會轉換成 Gray8 或 Bgr32。目前並沒有 PixelFormat 與 BitmapEncoder 的組合,可提供包含每原色 8 個位元以上之資料的檔案。
預乘 Alpha
PixelFormats 類別的三個靜態屬性都是以字母 P 開頭,P 代表預乘 Alpha (Premultiplied Alpha)。這項技術可用來提高像素為部分透明的點陣圖轉譯效率。它只適用於包含 Alpha 色頻的點陣圖。
假設您建立了一個色彩計算如下所示的 SolidColorBrush:
Color.FromArgb(128, 0, 0, 255)
這是包含 50 % 透明度的藍色筆刷。在繪製筆刷時,色彩必須結合顯示表面的現有色彩。在黑色背景上繪圖時,產生的 RGB 色彩會是 (0, 0, 128),而在白色背景上產生的色彩會是 (127, 127, 255)。這是簡單的加權平均計算。
下列公式中的註標指出在現有表面上繪製部分透明像素的結果:
Rresult = [(255 – Apixel) * Rsurface + Apixel * Rpixel] / 255;
Gresult = [(255 – Apixel) * Gsurface + Apixel * Gpixel] / 255;
Bresult = [(255 – Apixel) * Bsurface + Apixel * Bpixel] / 255;
如果像素的 R、G 及 B 值已經乘以 A 值並除以 255,就能加速這項計算作業。如此一來就可以省略每個公式中的第二次乘法。舉例而言,假設 Bgra32 點陣圖中的像素是 ARGB 值 (192, 40, 60, 255)。在 Pbgra32 點陣圖中,相同的像素就會是 (192, 30, 45, 192)。RGB 值已經乘以 Alpha 值 192 並除以 255。
至於 Pbgra32 點陣圖中的像素,R、G 或 B 值都不應該大於 A 值。若非如此,也並不會出問題。值會遵守 255 的上限,但是您將無法取得所需的透明度。
WriteableBitmap 應用程式
當您需要在應用程式中顯示一些簡單的動態圖形 (或許是橫條圖) 時,可能會使用 WriteableBitmap,並發現比起 WPF 繪製對應的向量圖形,更新點陣圖的速度更快。
WriteableBitmap 的最常見應用程式,應該是執行即時影像處理和非線性轉換。TwistedBitmap 專案可讓您載入任何每像素 8 個位元或每像素 32 個位元的點陣圖,並使用滑桿控制項來從影像中心扭轉影像,如 [圖 8] 所示。使用較小的影像可取得更佳的結果。
圖 8 扭轉的點陣圖 (按一下影像以放大圖片)
程式會使用 BitmapFrame.Create 從檔案載入點陣圖,然後呼叫 CopyPixels 來複製名為 pixelsSrc 之陣列中的所有像素位元。[圖 9] 中顯示的 SliderOnValueChanged 事件處理常式,會負責將像素從 pixelsSrc 轉換成名為 pixelsNew 的陣列,這會用來呼叫 WritePixels。

圖 9 TwistedBitmap 中的轉換
void SliderOnValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> args)
{
if (pixelsSrc == null)
return;
Slider slider = sender as Slider;
int width = bitmap.PixelWidth;
int height = bitmap.PixelHeight;
int xCenter = width / 2;
int yCenter = height / 2;
int bytesPerPixel = bitmap.Format.BitsPerPixel / 8;
for (int row = 0; row < bitmap.PixelHeight; row += 1)
{
for (int col = 0; col < bitmap.PixelWidth; col += 1)
{
// Calculate length of point to center and angle
int xDelta = col - xCenter;
int yDelta = row - yCenter;
double distanceToCenter = Math.Sqrt(xDelta * xDelta +
yDelta * yDelta);
double angleClockwise = Math.Atan2(yDelta, xDelta);
// Calculate angle of rotation for twisting effect
double xEllipse = xCenter * Math.Cos(angleClockwise);
double yEllipse = yCenter * Math.Sin(angleClockwise);
double radius = Math.Sqrt(xEllipse * xEllipse +
yEllipse * yEllipse);
double fraction = Math.Max(0, 1 - distanceToCenter / radius);
double twist = fraction * Math.PI * slider.Value / 180;
// Calculate the source pixel for each destination pixel
int colSrc = (int) (xCenter + (col - xCenter) *
Math.Cos(twist)
(row - yCenter) * Math.Sin(twist));
int rowSrc = (int) (yCenter + (col - xCenter) *
Math.Sin(twist)
+ (row - yCenter) * Math.Cos(twist));
colSrc = Math.Max(0, Math.Min(width - 1, colSrc));
rowSrc = Math.Max(0, Math.Min(height - 1, rowSrc));
// Calculate the indices
int index = stride * row + bytesPerPixel * col;
int indexSrc = stride * rowSrc + bytesPerPixel * colSrc;
// Transfer the pixels
for (int i = 0; i < bytesPerPixel; i++)
pixelsNew[index + i] = pixelsSrc[indexSrc + i];
}
}
// Write out the array
bitmap.WritePixels(rect, pixelsNew, stride, 0);
}
使用點陣圖轉換時,反向執行轉換是很重要的。若您查看原始點陣圖中的每個像素,以判斷這些像素應放在新點陣圖中的何處,可能會發現原始點陣圖中的不同像素會對應到新點陣圖中的相同像素。也就是說,新點陣圖中的某些像素不會設定任何值!影像將包含「漏洞」。
針對新點陣圖中的每個像素,您必須尋找原始點陣圖中的哪個像素是對應到特定的列和欄。這個方法可確保新點陣圖中的每個像素都有經過計算的值。