Image

原本我都是用產生 FB 封面的「康熙字典體產生器」來做簡單的文字 Banner,就拿來當作文章的封面照

用了好一陣子也沒啥問題。結果某天文章寫好,吃著火鍋唱著歌,產生器打開一看,服務竟然就沒了!

當下是一個震驚啊,一氣之下決定直接打開 Linqpad 寫一個。

註:現在搜尋康熙字典體產生器,還查得到介面截圖,還真的蠻簡單方便的 Q_Q

以前面的菜雞與物件導向系列 Banner 為例,我們大概需要:

  • 產生一張圖
  • 在圖上面放主標題和副標題
  • 關鍵字可以上色

Image

稍微搜尋一下發現 .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(); 
	}
}

Image

接著讓我們開始加入主標題,這邊就用先前 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();
	}
}

Image

我們順利地把標題顯示在圖的正中間,好像有那麼一點搞頭了。

接下來我們要加入關鍵字的顏色。原先的字典檔產生器可以用 % % 來把要標顏色的關鍵字框起來,例如:「使用 %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();
	}
}

Image

好的完蛋。

因為前面的 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();
	}
}

Image

看起來使用 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();
	}
}

Image

看起來主標題差不多有達到想要的效果了,順便多加了一組關鍵字測一下。程式碼的巢狀也越疊越多了哦

是時候來加個副標題了。副標題通常都比較簡單,像上面的範例只是用來標個系列文名稱

這邊直接抓主標題的程式碼稍微改一下,接在主標題後面操作就好

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);
}

Image

這樣就搞定啦!

最後稍微整理一下程式碼,把主標和副標會用到的參數包個類別,方便後面需要調整的時候可以用:

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);
}

Image

存完收工!

小結

雖然這個工具的場景挺侷限的,但寫小腳本的過程也還算有趣,雖然調位置的時候已經有點沒耐心,快要打開小畫家了

有鑒於三年前新訓的筆記到今天都還沒寫完,之後應該還是會派上用場 xD

但後來因緣際會看到另一種 Banner 製作方式令我躍躍欲試,結果忙半天又走回去用 Canva 直接產圖,又是別的故事了…