這一章講講ps與.net對象的二三事,將用一個小的實例說明下。
既然是.NET對象,那FRAMEWORK是必須的。我的機器上裝的是v2.0。這次主要用到的是System.Drawing命名空間。
那么.NET對象在哪?難不成可以直接使用?C#是要用using來引入命名空間的,ps引入.NET命名空間使用的是
[reflection.assembly]::LoadWithPartialName("命名空間")
其中assembly是在System.reflection這個命名空間下,ps會自動將System補上,所以直接用[reflection.assembly]就行了,而 "::" 是ps中使用對象(類或者結構體等)的靜態成員的操作符(后面可以接靜態方法,屬性),還記得字面變量嗎?其實說的就是[reflection.assembly]這種類型的變量,當時我覺得叫靜態變量更好些。相似的還有[DateTime],這里也將System省略了。例如你可以用
[DateTime]::Now
來獲取當前的時間。
好了,問題來了,system.reflection這個命名空間又是怎么引入的?這有點像雞生蛋的問題。其實有的命名空間當你打開ps的控制台的時候就已經在當前的領域中了。讀起來有點繞是吧?其實說領域是因為你可以用System這個命名空間下的AppDomain的一個靜態方法來查看當前領域中的程序集(assembly),其實把領域理解成當前的運行環境就好了。查看當前環境已引用的程序集的命令如下
[appdomain]::CurrentDomain.GetAssemblies()|%{$_.fullname}|Sort-Object
還記得命令管道吧,后面的兩個命令管道“|%{$_.fullname}“和”|Sort-Object“是為了格式化輸出到控制台的數據,其中%是foreach的別名,而foreach又是for-each的簡寫,sort-object是將管道內容排序,[appdomain]::CurrentDomain.GetAssemblies()就是獲取當前以引入的程序集的命令了。在沒有使用[reflection.assembly]::LoadWithPartialName("命名空間")引入其他程序集的情況下,我的輸出如下圖
其中我並沒有找到system.reflection這個命名空間,命名空間應該是不相交的,所以他到底怎么來的,對我來說還是個迷╮(╯▽╰)╭
開始說到了要用到System.Drawing這個命名空間,而默認是沒有的,所以需要執行下面的語句
[Reflection.assembly]::LoadWithPartialName("System.Drawing")
如果系統成功找到了該程序集,會有如下輸出
你可以讓[Reflection.assembly]::LoadWithPartialName("System.Drawing")返回空值既
[void][Reflection.assembly]::LoadWithPartialName("System.Drawing")
或者命令的輸出傳給一個空值既
[Reflection.assembly]::LoadWithPartialName("System.Drawing") >> $null
來取消輸出。而"System.Drawing"里的System是不能省略的,好吧,其實這些不重要...
今天要完成的小例子就是通過ps與.NET對象的交互來實現在控制台輸出一張圖片。先來科普下微軟控制台的顏色系統。
在視覺時代的今天微軟的控制台只能同時存在16種顏色,分別是ConsoleColor這個枚舉里的16個值
Black 黑色。
DarkBlue 藏藍色。
DarkGreen 深綠色。
DarkCyan 深紫色(深藍綠色)。
DarkRed 深紅色。
DarkMagenta 深紫紅色。
DarkYellow 深黃色(赭色)。
Gray 灰色。
DarkGray 深灰色。
Blue 藍色。
Green 綠色。
Cyan 青色(藍綠色)。
Red 紅色。
Magenta 紫紅色。
Yellow 黃色。
這16種顏色你可以通過右鍵控制台的標題欄選擇”默認值“或者“屬性”來看見,如下圖
當然,控制台不是調色板,我們不能怪微軟。不過其實控制台的默認顏色是可以改變的,只不過只能改成另外的16種而已,而他們的名字還是那16個枚舉值。這就有點說不過去了吧。比如你可以把console的顏色設置成如下這樣。
此時上述的16種顏色就全部變成了灰色系,也就是說consoleColor枚舉值的名字沒有改變,不過對應的顏色變了。例如[ConsoleColor]::Yellow對應的將是非常淺的灰色。下面說說console的字體大小,這將決定最后生成圖像的像素多少。因為我將一個字符代替一個像素,所以console可以輸出的字符數越多,就可以生成像素越高的圖像。
console默認的字符大小是8X16的點陣字體,也就是說一個字符的寬是8個像素,高是16個像素,我的顯示器是19寸(1440x900)的,也就是說在當前的console下只能生成最多像素值為90X55的位圖,基本就等於看馬賽克了。這里因為像素是一個帶顏色的正方形,所以實際要用兩個字符才能替代一個像素。
那我設置不就行了?當然可以,不過不幸的是在console的UI里最小的字符是設置成新宋體,而且大小是3X7的,效果應該類似於薄碼,而且無法用兩個字符剛好組成一個像素,因為無法構成完美的正方形,所以這樣設置的話最后出來的圖像會有一定的拉長的效果。如果希望設置在以后的窗體生效的話,點確定后請選擇“修改該窗口啟動的快捷方式”,如下圖
那怎么整,就這么做嗎?其實console的額外設置就在注冊表里,我一直覺得注冊表其中的一個作用就是配置文件。在“運行”輸入regedit之后,果然找到“HKEY_CURRENT_USER\Console%SystemRoot%_system32_WindowsPowerShell_v1.0_powershell.exe”(這個鍵的存在應該是我配置過powershell的原因,要是沒有這個鍵你也可以手動加一個,缺什么補什么)我的配置如下圖
我設置的FontSize的數據對應着新宋體的1號字,字體大小為1X2,貌似這個合適了。console的設置就此完成,我將這個鍵的注冊表導出得到如下的配置文本
Windows Registry Editor Version 5.00 [HKEY_CURRENT_USER\Console\%SystemRoot%_system32_WindowsPowerShell_v1.0_powershell.exe] "ScreenColors"=dword:0000000f "ColorTable02"=dword:00202020 "ColorTable03"=dword:00303030 "ColorTable04"=dword:00404040 "ColorTable05"=dword:00505050 "ColorTable06"=dword:00606060 "ColorTable07"=dword:00707070 "ColorTable09"=dword:00909090 "ColorTable10"=dword:00a0a0a0 "ColorTable11"=dword:00b0b0b0 "ColorTable12"=dword:00c0c0c0 "ColorTable13"=dword:00d0d0d0 "ColorTable14"=dword:00e0e0e0 "ColorTable15"=dword:00f0f0f0 "ScreenBufferSize"=dword:01a605a0 "WindowSize"=dword:01a605a0 "FontSize"=dword:00010000 "FontWeight"=dword:00000190 "FontFamily"=dword:00000036 "FaceName"="新宋體"
顏色我也已經設置成了灰色系,閑手動設置麻煩也可以吧上面的代碼粘到txt中改下名運行應該就ok了。好吧,下面開始進入正題。
------------------------------------------------------------------我是正題的分割--------------------------------------------------------------------------------------
前面的程序集已經導入了,那如何在powershell中創建一個.NET呢?就是用到new-object這個命令了,既然是和位圖交互,那先創建個位圖的對象
[Drawing.Image]$image=New-Object drawing.bitmap($sPath)
這里使用的是bitmap構造函數的一個重載,$sPath是位圖的路徑
但有的位圖可是很大的,絕對大過現在我的console能呈現的像素大小(720X450)所以要先判斷下位圖的大小,與console的窗口比較下。console的相關屬性可以用[Console]這個靜態類來調用。放縮位圖的大小可以使用image類的GetThumbnailImagez這個實例方法。代碼如下
#[Console]::LargestWindowHeight獲取console的最大高度為實際的像素高度,因為[Console]的高度和寬度值不是橫屏或縱屏的像素,而是實際可以容納的字符個數,所以高度就不用/2
[int]$pixHeight=[Math]::Floor([Console]::LargestWindowHeight)
#[Console]::LargestWindowHeight獲取console的最大寬度/2為實際的像素寬度
[int]$pixWidth=[Math]::Floor([Console]::LargestWindowWidth/2)
if(($image.Width -gt [int]$pixWidth) -or ($image.Height -gt [int]$pixHeight))
{
#放縮比例為高度和寬度對比實際比例較大的一個
$rate=[Math]::Max($image.Width/$pixWidth,$image.Height/[int]$pixHeight)
#放縮
$image=$image.GetThumbnailImage([Math]::Floor(($image.Width)/$rate),[Math]::Floor(($image.Height)/$rate),$null,0)
}
圖片處理完畢,可以直接用image的getpixel這個實例方法獲取每一個點的像素的RGB分量值,然后使用Grey=R*0.7+B*0.2+G*0.1這個灰度公式(此公式來自百度)將像素逐一灰化,公式的系數可以自己定。由於灰色系的R,G,B值都是一樣的,所以只要得出一個灰度就可以確定這個像素點灰化后的顏色值。最后再用灰度和[consoleColor]比較就可以確定與該像素點顏色最近似的灰色系顏色。代碼如下
#得到原像素點的灰度 $greyDegree=($imColor.R)*0.7+($imColor.G)*0.2+($imColor.B)*0.1 #這里的seed=16,因為我為了方便,將灰色的進階值設為了16(256色/console可以呈現的顏色數16),及灰色按(0,0,0);(16,16,16);(32,32,32)這樣遞增 #得到初始的偏移值用於比較 $offset=[Math]::Abs($greyDegree-0*$global:seed) #$global:siColor是一個全局數組,里面保存了16種控制台顏色的枚舉(名字一樣,顏色卻不同了),設置一個初始的顏色,對應初始偏移值 $fbColor=($global:siColor[0]) for($i=0;$i -lt ($global:siColor.Length);$i++) { #遍歷全局數組,求出一個臨時偏移值 $tempOffset=[Math]::Abs($greyDegree-$i*$seed) if($tempOffset -lt $offset) { #如果臨時偏移值小於初始偏移值,說明當前的偏移值更加近似原像素灰度,交換~繼續比較直到找出最近似的 $offset=$tempOffset $fbColor=($global:siColor[$i]) } }
接下來就是呈現的問題了,這個問題我糾結了很久,開始我是用write-host這個命令配合-foregroupcolor,-backgroupcolor,-nonewline這幾個參數配合一個一個以對應的近似灰色值作為前景和背景色輸出一個字符來作為像素,不過這樣一個像素一個像素輸出確實比較惡心,后來又想先把所有的像素值保存在緩存中,最后一次輸出,可是當像素值比較多的時候要等很長的時間。最后還是決折中,一行一行輸出。
先說說呈現時使用到的命名空間System.Management.Automation.Host,這是powershell默認會導入的一個命名空間,所以不用重新加載。
可以使用(get-host).UI.RAWUI這個命令來得到此命名空間中的一個對象,這個對象也就是當前的powershell的host的UI,這里也就是console。得到了這個RAWUI對象之后可以使用SetBufferContents($pPos,$rRectscreen)這個重載方法將一個保存着緩存單元的矩陣數組以console的某一個點作為這個矩陣數組的左上角(也就是矩陣數組第一個元素)將該矩陣數組輸出到屏幕上。這一段我自己都把自己繞糊塗了。
簡單點說,SetBufferContents($pPos,$rRectscreen)這個重載方法有兩個參數,一個是二維坐標$pPos(這是System.Management.Automation.Host下的一個結構以),對應着console中的某一個點,其中(0,0)表示的坐標是console的左上角。而$rRectscreen保存着一個矩陣數組(不是二維數組,二維數組用array[index][index]獲取元素,而矩陣數組使用array[a,b]其中a,b表示的應該是向量,具體的區別比如在內存中的存儲方式什么的我真不知道,求解惑。總之二者不是一回事)。這個矩陣數組的每一個元素都是一個buffercell對象,這個對象保存着一個字符,這個字符有前景色和背景色等屬性,具體細節請MSDN。所以我們的目標是將一個二維圖像的每一個像素進行灰化處理后保存在一個矩陣數組中,當一行像素保存完成后,用SetBufferContents將這行像素輸出到屏幕的指定位置。
其他的比較簡單,而要在ps中得到一個矩陣數組卻有點麻煩,至今我們找到直接在ps里創建一個矩陣數組的方法,開始我是用RAWUI的GetBufferContents($Rect)這個方法返回的一個矩陣數組,然后再操作這個矩陣數組。$Rect是System.Management.Automation.Host下的另一個結構體。不過這樣實在讓我不爽,取到了屏幕上的一個矩陣數組里的值結果直接把他丟棄了,要的是容器而已。有點買櫝還珠的意思。所以這個方法我就不推薦了,實現詳情請google
經過一番探索之后,我終於通過反射的機制實現了我想要的,具體的代碼(包括如何呈現如下)
#獲取System.Management.Automation.Host.BufferCell[,]類型 $bufferRect=[Type]::GetType("System.Management.Automation.Host.BufferCell[,]") #獲取Int類型 $paramType=[Type]::GetType("System.Int32") #初始化一個參數數組,用於作為GetConstructor的參數,來獲取一個"System.Management.Automation.Host.BufferCell[,]"的需要兩個Int類型作為參數的構造函數 $paramTypes=@($paramType,$paramType) #獲取"System.Management.Automation.Host.BufferCell[,]"的其中一個構造函數,這個構造函數接受兩個Int型作為參數 $ctor =$bufferRect.GetConstructor($paramTypes) #初始化將傳給構造函數的參數數組 $param=@(1,(($iImage.Width)*2)) #通過構造函數實例化一個矩陣數組(BufferCell[,]) $rRectscreen=$ctor.Invoke($param) #創建一個BufferCell對象,將被存在矩陣數組(BufferCell[,])中 $bcCell=New-Object "System.Management.Automation.Host.BufferCell" #設置bufferCell的字符屬性,可以是任何英文字符 $bcCell.Character="R" #把這個BufferCell的前景色和背景色設置為同一顏色,作為一個像素的組成部分 $bcCell.ForegroundColor=$fbColor $bcCell.BackgroundColor=$fbColor #將這個BufferCell裝入矩陣數組(BufferCell[,])中,因為console字體的關系所以兩個BufferCell將組成一個像素 $rRectscreen[0,($j*2)]=$bcCell $rRectscreen[0,(($j*2)+1)]=$bcCell
上面貼的程序是核心部分,最后將所有程序組合在一起是這個樣子滴
function GetImage { param([String]$p) [Drawing.Image]$image=New-Object drawing.bitmap($sPath) #[Console]::LargestWindowHeight獲取console的最大高度/2為實際的像素高度 [int]$pixHeight=[Math]::Floor([Console]::LargestWindowHeight/2) #[Console]::LargestWindowHeight獲取console的最大寬度/2為實際的像素寬度 [int]$pixWidth=[Math]::Floor([Console]::LargestWindowWidth/2) if(($image.Width -gt [int]$pixWidth) -or ($image.Height -gt [int]$pixHeight)) { #放縮比例為高度和寬度對比實際比例較大的一個 $rate=[Math]::Max($image.Width/$pixWidth,$image.Height/[int]$pixHeight) #放縮 $image=$image.GetThumbnailImage([Math]::Floor(($image.Width)/$rate),[Math]::Floor(($image.Height)/$rate),$null,0) } return $image } function GetColor { param([Int]$R,[Int]$G,[Int]$B) #得到原像素點的灰度 $greyDegree=($imColor.R)*0.7+($imColor.G)*0.2+($imColor.B)*0.1 #這里的seed=16,因為我為了方便,將灰色的進階值設為了16(256色/console可以呈現的顏色數16),及灰色按(0,0,0);(16,16,16);(32,32,32)這樣遞增 #得到初始的偏移值用於比較 $offset=[Math]::Abs($greyDegree-0*$global:seed) #$global:siColor是一個全局數組,里面保存了16種控制台顏色的枚舉(名字一樣,顏色卻不同了),設置一個初始的顏色,對應初始偏移值 $fbColor=($global:siColor[0]) for($i=0;$i -lt ($global:siColor.Length);$i++) { #遍歷全局數組,求出一個臨時偏移值 $tempOffset=[Math]::Abs($greyDegree-$i*$seed) if($tempOffset -lt $offset) { #如果臨時偏移值小於初始偏移值,說明當前的偏移值更加近似原像素灰度,交換~繼續比較直到找出最近似的 $offset=$tempOffset $fbColor=($global:siColor[$i]) } } return $fbColor } $sPath="C:\Documents and Settings\Administrator\桌面\fd2.jpg" if(!(Test-Path $sPath)) { Write-Host "path not found" exit } $global:seed=16 $global:siColor=@("Black","DarkBlue","DarkGreen","DarkCyan","DarkRed","DarkMagenta","DarkYellow","Gray","DarkGray","Blue","Green","Cyan","Red","Magenta","Yellow","White") [void][reflection.assembly]::LoadWithPartialName("System.Drawing") [Console]::WindowHeight=[Console]::LargestWindowHeight [Console]::WindowWidth=[Console]::LargestWindowWidth $uConsoleHostRawUI=(Get-Host).UI.RawUI $iImage=GetImage -p $sPath #獲取System.Management.Automation.Host.BufferCell[,]類型 $bufferRect=[Type]::GetType("System.Management.Automation.Host.BufferCell[,]") #獲取Int類型 $paramType=[Type]::GetType("System.Int32") #初始化一個參數數組,用於作為GetConstructor的參數,來獲取一個"System.Management.Automation.Host.BufferCell[,]"的需要兩個Int類型作為參數的構造函數 $paramTypes=@($paramType,$paramType) #獲取"System.Management.Automation.Host.BufferCell[,]"的其中一個構造函數,這個構造函數接受兩個Int型作為參數 $ctor =$bufferRect.GetConstructor($paramTypes) #初始化將傳給構造函數的參數數組 $param=@(1,(($iImage.Width)*2)) #通過構造函數實例化一個矩陣數組(BufferCell[,]) $rRectscreen=$ctor.Invoke($param) #創建一個BufferCell對象,將被存在矩陣數組(BufferCell[,])中 $bcCell=New-Object "System.Management.Automation.Host.BufferCell" #設置bufferCell的字符屬性,可以是任何英文字符 $bcCell.Character="R" $pPos=$uConsoleHostRawUI.WindowPosition for($i=0;$i -lt $iImage.Height;$i++) { for($j=0;$j -lt $iImage.Width;$j++) { [drawing.color]$imColor=$iImage.GetPixel($j,$i) $fbColor=GetColor -R $imColor.R -G $imColor.G -B $imColor.B #把這個BufferCell的前景色和背景色設置為同一顏色,作為一個像素的組成部分 $bcCell.ForegroundColor=$fbColor $bcCell.BackgroundColor=$fbColor #將這個BufferCell裝入矩陣數組(BufferCell[,])中,因為console字體的關系所以兩個BufferCell將組成一個像素 $rRectscreen[0,($j*2)]=$bcCell $rRectscreen[0,(($j*2)+1)]=$bcCell } $pPos.x=0 $pPos.y=$i $uConsoleHostRawUI.SetBufferContents($pPos,$rRectscreen) } Read-host
還有以下幾點需要說明
(1)最后用一個Read-Host來等待用戶輸入,以免程序運行完成之后直接退出。
(2)$sPath設置成你要輸出在控制台的文件路徑即可
(3)實例化矩陣數據的程序沒有放在一個函數里是因為ps神奇的返回值方式(將返回值裝在一個object[]中)而又無法再將返回值還原成矩陣數據(其他的類型String什么的卻可以)
(4)算法和代碼結構都比較挫,能理解ps和.NET對象的交互就好。
(5)將源文件保存為.PS1的然后-右鍵-使用powershell運行(因為我們只在注冊表里更改了powershell的console配置,而且讓你在1x2的字體下敲命令我估計也不太現實)
最后是效果圖,先是永遠的飯島愛老師
然后是經過放縮后的walle
其他的.NET類的使用大同小異。中秋快樂~