使用 .Net 的 System.Drawing 產生簡單的文字 Banner 初體驗
原本我都是用產生 FB 封面的「康熙字典體產生器」來做簡單的文字 Banner,就拿來當作文章的封面照
用了好一陣子也沒啥問題。結果某天文章寫好,吃著火鍋唱著歌,產生器打開一看,服務竟然就沒了!
當下是一個震驚啊,一氣之下決定直接打開 Linqpad 寫一個。
註:現在搜尋康熙字典體產生器,還查得到介面截圖,還真的蠻簡單方便的 Q_Q
以前面的菜雞與物件導向系列 Banner 為例,我們大概需要:
- 產生一張圖
- 在圖上面放主標題和副標題
- 關鍵字可以上色
稍微搜尋一下發現 .Net 已經有 System.Drawing 這個工具可以幫我們完成這些簡單的圖片任務,事不宜遲馬上就來嘗試!
開工
首先第一步,當然是先畫出橫幅底圖,這邊就讓我們先建立圖片,並且用 Graph.Clear
先塗個黑色好辨識,長寬就參照先前產生器的長 315 寬 850:
void Main()
{
int width = 850;
int height = 315;
using (var img = new Bitmap(width, height))
{
using (var graph = Graphics.FromImage(img))
{
// 先塗個黑色當底
graph.Clear(Color.Black);
}
// Linqpad 的打印語法,可以直接顯示圖片
img.Dump();
}
}
接著讓我們開始加入主標題,這邊就用先前 Fluent Validation 文章的標題當作示範,直接用 Graph.DrawString
把文字寫到圖片中間。
void Main()
{
int width = 850;
int height = 315;
// 主標題相關設定:字型、大小、顏色
var title = "使用 Fluent Validation \n來驗證參數吧";
var fontName = "Hanyi Senty Chalk Original";
var fontSize = 36;
var fontColor = Color.White;
using (var img = new Bitmap(width, height))
{
using (var graph = Graphics.FromImage(img))
{
graph.Clear(Color.Black);
// 建立一下文字和筆刷
var font = new Font(fontName, fontSize);
var brush = new SolidBrush(fontColor);
// 設定一下位置,Banner 的字當然要置中
var rect = new Rectangle(0, 0, width, height);
var format = new StringFormat()
{
Alignment = StringAlignment.Center,
LineAlignment = StringAlignment.Center
};
// 直接把整個標題畫到圖上
graph.DrawString(title, font, brush, rect, format);
}
img.Dump();
}
}
我們順利地把標題顯示在圖的正中間,好像有那麼一點搞頭了。
接下來我們要加入關鍵字的顏色。原先的字典檔產生器可以用 % %
來把要標顏色的關鍵字框起來,例如:「使用 %Fluent Validation% 來驗證參數吧」這樣就知道「Fluent Validation」是關鍵字,需要上色。
因此這邊最快的做法就是用 %
來切割,再照著「一般顏色、%
、關鍵字顏色、%
、一般顏色」的順序輪流上色就好了。上色也只需要替換設定筆刷的 SolidBrush
就好,非常簡單:
void Main()
{
int width = 850;
int height = 315;
var title = "使用%Fluent Validation%\n來驗證參數吧";
var fontName = "Hanyi Senty Chalk Original";
var fontSize = 36;
var fontColor = Color.White;
// 加入關鍵字要用的第二組文字顏色
var fontSubColor = Color.Goldenrod;
using (var img = new Bitmap(width, height))
{
using (var graph = Graphics.FromImage(img))
{
graph.Clear(Color.Black);
var font = new Font(fontName, fontSize);
var brush = new SolidBrush(fontColor);
// 加入了關鍵字用的第二組筆刷
var subBrush = new SolidBrush(fontSubColor);
var rect = new Rectangle(0, 0, width, height);
var format = new StringFormat()
{
Alignment = StringAlignment.Center,
LineAlignment = StringAlignment.Center
};
// 為了照一般色、關鍵字色、一般色的順序上色,需要先把標題做拆分
var words = title.Split('%');
for (var i = 0; i < words.Count(); i++)
{
// 有 %% 圈選起來的字詞,使用特殊色 ex: %API%
var thisBrush = i % 2 == 0
? brush
: subBrush;
// 改成分別將每一組字畫到圖上
graph.DrawString(words[i], font, thisBrush, rect, format);
}
}
img.Dump();
}
}
好的完蛋。
因為前面的 Graph.DrawString
是無腦把整個 title
丟進去產生,因此文字的位置只需要在 StringFormat
指定個 Center
讓他置中就沒問題了。但
現在又要切關鍵字啥的,全部指定在中間當然就會疊在一起囧。這下子就只能自行計算長度了。
因為用 %
切完關鍵字之後會有很多段字,最直接的做法還是先嘗試抓到上一段字的結束位置,再繼續寫下一段字在後面,這時候我們就可以用上計算文字大小的 graph.MeasureString
,馬上進行調整:
void Main()
{
int width = 850;
int height = 315;
var title = "使用%Fluent Validation%\n來驗證參數吧";
var fontName = "Hanyi Senty Chalk Original";
var fontSize = 36;
var fontColor = Color.White;
var fontSubColor = Color.Goldenrod;
using (var img = new Bitmap(width, height))
{
using (var graph = Graphics.FromImage(img))
{
graph.Clear(Color.Black);
var font = new Font(fontName, fontSize);
var brush = new SolidBrush(fontColor);
var subBrush = new SolidBrush(fontSubColor);
var rect = new Rectangle(0, 0, width, height);
var format = new StringFormat()
{
// 放棄直接指定置中的語法
//Alignment = StringAlignment.Center,
//LineAlignment = StringAlignment.Center
};
var words = title.Split('%');
for (var i = 0; i < words.Count(); i++)
{
// 有 %% 圈選起來的字詞,使用特殊色 ex: %API%
var thisBrush = i % 2 == 0
? brush
: subBrush;
graph.DrawString(words[i], font, thisBrush, rect, format);
// 嘗試計算每一段字的長度,然後重新調整位置
// 讓下一段字可以接著繼續寫
var wordWidthFloat = graph.MeasureString(words[i], font).Width;
var wordWidth = Convert.ToInt16(wordWidthFloat);
rect.Offset(wordWidth, 0);
}
}
img.Dump();
}
}
看起來使用 Graph.MeasureString
先抓到字段的長度再繼續寫下去是可行的。同時我們也知道了幾件事:
因為 StringAlignment.Center
的置中已經被拿掉了,現在我們除了要自行計算長度,還得要自己指定起始位置了,不然會跑回左上角。這時候就需要回來調整處理位置的 Rectangle
。
幸好圖片的長寬都是我們先知道的,所以這部分直接用整個 Banner 的長度除以二先定位到正中間,然後再往左邊移整行字的長度的一半,這樣就可以讓這行字置中且左右對稱了!
除了起始位置以外,還可以看到 \n
切出去的第二行字的開始位置也會被第一行字的結束位置影響到,這代表我們換行顯示的時候需要重新調整位置。
至於第一行要顯示的高度,就要直接測一下了。
void Main()
{
int width = 850;
int height = 315;
var title = "使用%Fluent Validation%\n來%驗證參數%吧";
var fontName = "Hanyi Senty Chalk Original";
var fontSize = 36;
var fontColor = Color.White;
var fontSubColor = Color.Goldenrod;
using (var img = new Bitmap(width, height))
{
using (var graph = Graphics.FromImage(img))
{
graph.Clear(Color.Black);
var font = new Font(fontName, fontSize);
var brush = new SolidBrush(fontColor);
var subBrush = new SolidBrush(fontSubColor);
var rect = new Rectangle(0, 0, width, height);
var format = new StringFormat();
// 先把每一行字都拆開,方便後續換行處理
var contents = title.Split('\n');
var lines = 0;
foreach (var content in contents)
{
// 每一行的開頭重新定位起始位置
var contentWidthFloat = graph.MeasureString(content, font).Width;
var contentWidth = Convert.ToInt16(contentWidthFloat);
// 先從整張圖的寬度抓到正中間,再往左移動這行字的一半,讓整行可以置中
var setX = (width / 2) - (contentWidth / 2);
// 先抓整張圖中間偏上的位置,然後換行的時候就往下移動一個字的高度
var setY = height / 5 + lines * font.Height;
// 中英文混著抓距離的話有時候點歪歪的= = 這邊加個值方便手動校正起始點
var widthset = 20;
// 重新定位要寫字上去的起始點
rect.Location = new Point(setX + widthset, setY);
var words = content.Split('%');
for (var i = 0; i < words.Count(); i++)
{
var thisBrush = i % 2 == 0
? brush
: subBrush;
graph.DrawString(words[i], font, thisBrush, rect, format);
var wordWidthFloat = graph.MeasureString(words[i], font).Width;
var wordWidth = Convert.ToInt16(wordWidthFloat);
rect.Offset(wordWidth, 0);
}
lines++;
}
}
img.Dump();
}
}
看起來主標題差不多有達到想要的效果了,順便多加了一組關鍵字測一下。程式碼的巢狀也越疊越多了哦
是時候來加個副標題了。副標題通常都比較簡單,像上面的範例只是用來標個系列文名稱
這邊直接抓主標題的程式碼稍微改一下,接在主標題後面操作就好
var subTitle = "菜雞新訓記‧七";
var subFontSize = 20;
// 中間處理主標題的部分省略...
// 副標題
if (subTitle != null &&
string.IsNullOrEmpty(subTitle) is false)
{
// 副標題的字型,只有大小先跟主標題不一樣,高度則往下抓一些避免蓋到主標
var subFont = new Font(fontName, subFontSize);
var subRect = new Rectangle(0, height - height / 4, width, height);
var subFormat = new StringFormat()
{
Alignment = StringAlignment.Center,
LineAlignment = StringAlignment.Near
};
graph.DrawString(subTitle, subFont, brush, subRect, subFormat);
}
這樣就搞定啦!
最後稍微整理一下程式碼,把主標和副標會用到的參數包個類別,方便後面需要調整的時候可以用:
public class BannerString
{
public string FontName { get; set; } = "Hanyi Senty Chalk Original";
public string Content { get; set; } = string.Empty;
public int FontSize { get; set; } = 36;
public Color MainColor { get; set; } = Color.White;
public Color SubColor { get; set; } = Color.Goldenrod;
}
製作 Banner 的語法也先裝起來,改成傳遞上面包好類別的函式。
用來調整位置的偏移值也抽出來放著方便隨時調整:
private readonly int widthset = 20;
public void GenerateBanner(
BannerString title,
BannerString subTitle = null,
int width = 850,
int height = 315)
{
using (var img = new Bitmap(width, height))
{
using (var graph = Graphics.FromImage(img))
{
// 上面的程式碼本體
}
}
}
這樣後續回來使用這個腳本就可以直接改外面的參數就好啦!
void Main()
{
var title = new BannerString
{
Content = "使用%Fluent Validation%\n來%驗證參數%吧",
FontSize = 40
};
var subTitle = new BannerString
{
Content = "菜雞新訓記‧七",
FontSize = 20
};
GenerateBanner(title, subTitle);
}
存完收工!
小結
雖然這個工具的場景挺侷限的,但寫小腳本的過程也還算有趣,雖然調位置的時候已經有點沒耐心,快要打開小畫家了
有鑒於三年前新訓的筆記到今天都還沒寫完,之後應該還是會派上用場 xD
但後來因緣際會看到另一種 Banner 製作方式令我躍躍欲試,結果忙半天又走回去用 Canva 直接產圖,又是別的故事了…
其他文章
哈囉,如果你也有 LikeCoin,也覺得我的文章有幫上忙的話,還請不吝給我拍拍手呦,謝謝~ ;)