既定のプリンターと用紙サイズの変更

タグの編集
投稿者 まこ  (社会人) 投稿日時 2022/2/19 00:38:22
環境:Windows10、WindowsFormアプリ、.NET4.8

Windows既定のプリンターの変更と用紙サイズの変更をVB.NETから行いたいです。
調査したら↓のPowerShellを使った方法が見つかったのですが、PowerShellも
使ったことがないので、コードから利用するのに苦戦しています。

https://qiita.com/arachan@github/items/438f4cd806d445aa8ce5

試行錯誤して以下のような所まで自分でやってみたのですが、
ここから先に進めません。お助けください。
System.Management.Automationの参照はできています。


【既定のプリンターの変更】
PowerShellからは↓で変更できました。プリンタはとりあえず「DocuWorks Printer」という仮想プリンタにしています。
ここはインストールされているプリンタからComboBox等で選べるようにするつもりです。
 
$Printer=Get-WmiObject Win32_Printer | Where-Object Name -eq "DocuWorks Printer"
$Printer.SetDefaultPrinter()

デバッガで確認すると、変数printerはコレクションで1つだけ要素が格納されていました。
 
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Dim runspaceInvoke As RunspaceInvoke = New RunspaceInvoke()
        Dim printer As Object = runspaceInvoke.Invoke("Get-WmiObject Win32_Printer | Where-Object Name -eq ""DocuWorks Printer""")
        printer.SetDefaultPrinter() '★ここでエラー printer(0).SetDefaultPrinter()でもNG 
    End Sub

【用紙サイズの変更】
こちらはPowerShellからは↓でもNG。
確認は[設定]  > [デバイス] > [プリンターとスキャナー] で該当プリンタの「印刷設定」で確認しました。
できればA3からA4に縮小印刷する場合の設定方法があれば、それも教えて欲しいです。
 
$Printers=Get-WmiObject Win32_Printer
$Printer=$Printers | Where-Object Name -eq "DocuWorks Printer"
$Printer.DefaultPaperType="A3"
$Printer.Put()


投稿者 るきお  (社会人) 投稿日時 2022/2/19 16:58:46
Windows API の SetDefaultPrinterを使用すると既定のプリンターを設定できます。
この例はWindowsフォームアプリケーションで作成しています。

Public Class Form1

    <Runtime.InteropServices.DllImport("winspool.drv")>
    Public Shared Function SetDefaultPrinter(Name As StringAs Boolean
    End Function

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

        '参考:インストールされているプリンターの名前を確認する方法 
        For Each printerName As String In Printing.PrinterSettings.InstalledPrinters
            Debug.WriteLine(printerName)
        Next

        '既定のプリンターのセット 
        SetDefaultPrinter("DocuWorks Printer")

    End Sub

End Class


WMIを使う方法もあると思います。

用紙サイズの方は話が込み入っています。

>確認は[設定]  > [デバイス] > [プリンターとスキャナー] で該当プリンタの「印刷設定」で確認しました。
この設定はプリンターごとに違う部分になるかもしれないので、使用されているプリンターのデバイスドライバーの仕様を確認する必要があるかもしれません。

プリンターにこだわらず用紙サイズを指定して印刷できれば良いのであれば、WindowsフォームアプリケーションでPrintDocumentコントロールのDefaultPageSettings.PaperSizeで指定することは可能です。
PrintDocument1.DefaultPageSettings.PaperSize = New Printing.PaperSize("カスタムサイズ", 400, 400)


https://docs.microsoft.com/ja-jp/dotnet/api/system.drawing.printing.pagesettings.papersize?view=netframework-2.0
投稿者 まこ  (社会人) 投稿日時 2022/2/19 21:27:14
るきお 様、ありがとうございます。

SetDefaultPrinter APIを使った方法で「既定のプリンタの変更」はできました。

参考にしたページでは、PowerShellを使って「用紙サイズ」や「向き」の変更もできているようなので
何か方法はあるのかな、と思いましたが、OSバージョンやプリンタ機種による相違かもしれません。
当然、プリンタドライバの違いで設定できる項目に相違があることは承知しております。
自分の環境にインストールされているプリンタはPowerShellでも軒並みNGでした。
こちらの方は、半分、あきらめています。

只、「既定のプリンタの変更」の方は折角、APIによる方法を教えてもらったのですが、
PowerShellでも変更できているので、コードでのSystem.Management.Automationによる方法で
実現可能と考えています。
後学のために今、「PSObject Class」について調べていて、もう少し粘ってみます。
投稿者 るきお  (社会人) 投稿日時 2022/2/19 22:28:51
>参考にしたページでは、PowerShellを使って「用紙サイズ」や「向き」の変更もできているようなので
何か方法はあるのかな、と思いました
言われてみればそうですね。

WMIでやってみました。このプログラムを実行しても用紙サイズは変わりませんでした。
'要 Option Strict Off 
Using printer As New Management.ManagementObject("Win32_Printer.DeviceID=""Microsoft Print to PDF""")
    printer.Get() '一応Getしてみても特に変わらず 

    Debug.WriteLine("参考:このプリンターで使用できるプロパティの一覧")
    For Each prop In printer.Properties
        Debug.WriteLine($"{prop.Name} = {prop.Value}")
    Next

    printer("DefaultPaperType") = "A5"
    printer.Put()
End Using


ただ、私の場合、まこさんが提示されたサイトのPowerShellでも用紙サイズは変わらなかったです。エラーにもなりません。
だから、ひょっとするとまこさんの環境ではうまくいくのかもしれませんね。
なお、上述のプログラムもPowerShellも実行するには「管理者として実行」する必要がありました。

参考:別バージョン
こちらも結果は変わらずです。
'要 Option Strict Off 
Using mc As New Management.ManagementClass("Win32_Printer")
    Using printers As System.Management.ManagementObjectCollection = mc.GetInstances()

        Dim printer As Management.ManagementObject = Nothing

        For Each p In printers
            If p("Name") = "Microsoft Print to PDF" Then
                printer = p
                Exit For
            End If
        Next

        Debug.WriteLine("参考:このプリンターで使用できるプロパティの一覧")
        For Each prop In printer.Properties
            Debug.WriteLine($"{prop.Name} = {prop.Value}")
        Next

        printer("DefaultPaperType") = "A5"

        printer.Put()
        printer.Dispose()

    End Using
End Using

投稿者 るきお  (社会人) 投稿日時 2022/2/19 22:35:24
まこさんも、用紙の変更の方はPowerShellでもできていないと書かれていましたね。
投稿者 まこ  (社会人) 投稿日時 2022/2/19 22:50:22
System.Management.Automationによる方法で、「既定プリンタの変更」は実現できました。
変数の名前がダメダメですが....

 Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Dim runspaceInvoke As RunspaceInvoke = New RunspaceInvoke()
        Dim printer As Object = runspaceInvoke.Invoke("Get-WmiObject Win32_Printer | Where-Object Name -eq ""CubePDF""")
        Dim v As PSObject = printer(0)
        Dim vv As Management.ManagementObject = v.ImmediateBaseObject
        vv.InvokeMethod("SetDefaultPrinter"Nothing)
    End Sub
投稿者 るきお  (社会人) 投稿日時 2022/2/19 22:51:40
どうしてもプリンターの設定を変更しようと思うと、DEVMODE構造体まわりの話になってくると思うのですよね。

https://docs.microsoft.com/ja-jp/dotnet/api/system.drawing.printing.printersettings.gethdevmode?view=dotnet-plat-ext-6.0

この構造体は本当に意地悪で多くのプログラマーを苦しめているんです。
そして、引用したサイトに書いてある
>DEVMODE 構造体はデバイス固有です。
の部分が、私が最初に書いた
>この設定はプリンターごとに違う部分になるかもしれないので、使用されているプリンターのデバイスドライバーの仕様を確認する必要があるかもしれません。
の部分です。

過去にチャレンジした人の名残がネット上に見つかりました。
http://bbs.wankuma.com/index.cgi?mode=al2&namber=2377&KLOG=2
https://atmarkit.itmedia.co.jp/bbs/phpBB/viewtopic.php?topic=42909&forum=7
https://dobon.net/vb/bbs/log3-47/28144.html

デバイスごとに仕様が違うとすると、あるプリンターで成功しても別のプリンターでは通用しないということがありえます。
だから、既定の用紙サイズを変更する方法はこれだという決定的な情報が古していないのだと思います。

VB6版ですがサンプルを公開している人はいました。
この方はカスタムサイズを設定する方法を公開しているようです。
https://binaryworld.net/main/CodeDetail.aspx?CodeId=3093
プログラムが複雑なので中身は見ていないのですが、どのプリンターでも通用するものなのかどうかは疑問です。
投稿者 るきお  (社会人) 投稿日時 2022/2/19 22:53:46
>System.Management.Automationによる方法で、「既定プリンタの変更」は実現できました。
既定のプリンターの変更は、デバイスごとの違いもなく、私が提示したWindows APIのプログラムでもできています。

問題は既定の用紙のサイズの変更の方です。
こちらは、PowerShell版で私もまこさんもできておらず、私の.NET版のプログラムでもできていません。

私が思っていることは前述のとおりです。
投稿者 まこ  (社会人) 投稿日時 2022/2/19 23:15:37
るきお様、了解です。
色々、検証していただき、ありがとうございます。

自分もそこまで、こだわるわけではないので......
興味があったのは、PowerShellの文字列を使って、コードに移植する事だったので。
これで、終わりにしたいと思います。

どうも、ありがとうございました。

PS. RunspaceInvokeはIDisposableを実装しているので、Usingした方がいいかも。
投稿者 KOZ  (社会人) 投稿日時 2022/2/20 20:58:50
デフォルトプリンタの用紙設定を変更するサンプルです。
これで設定変更できないでしょうか?
# 長いので API 宣言は後述

Option Strict On

Imports System.ComponentModel
Imports System.Drawing.Printing
Imports System.Runtime.InteropServices

Module Module1

    Sub Main()
        'デフォルトプリンタの用紙設定を行う 
        Dim ps As New PrinterSettings
        For Each paperSize As PaperSize In ps.PaperSizes
            If paperSize.Kind = PaperKind.A3 Then
                ps.DefaultPageSettings.PaperSize = paperSize
                Exit For
            End If
        Next

        Dim pd As New PRINTER_DEFAULTS()
        Dim hPrinter As IntPtr

        pd.DesiredAccess = PRINTER_ALL_ACCESS
        If Not OpenPrinter(ps.PrinterName, hPrinter, pd) Then
            Throw New Win32Exception()
        End If

        Try
            Dim pi2 As PRINTER_INFO_2 = GetPrinterInfo2(hPrinter)
            Dim hMem As IntPtr = ps.GetHdevmode()
            Try
                pi2.pDevMode = GlobalLock(hMem)
                SetPrinter(hPrinter, 2, pi2, 0)
            Finally
                GlobalUnlock(hMem)
                GlobalFree(hMem)
            End Try
        Finally
            ClosePrinter(hPrinter)
        End Try
    End Sub

    Private Function GetPrinterInfo2(hPrinter As IntPtr) As PRINTER_INFO_2
        Dim needed As Integer
        GetPrinter(hPrinter, 2, IntPtr.Zero, 0, needed)
        If needed <= 0 Then
            Throw New Win32Exception()
        End If
        Dim pPrinterInfo As IntPtr = Marshal.AllocHGlobal(needed)
        Try
            Dim temp As Integer
            If Not GetPrinter(hPrinter, 2, pPrinterInfo, needed, temp) Then
                Throw New Win32Exception()
            End If
            Return Marshal.PtrToStructure(Of PRINTER_INFO_2)(pPrinterInfo)
        Finally
            Marshal.FreeHGlobal(pPrinterInfo)
        End Try
    End Function

End Module
投稿者 KOZ  (社会人) 投稿日時 2022/2/20 21:00:08
Module NativeMethods
    Public Const STANDARD_RIGHTS_REQUIRED As Integer = &HF0000
    Public Const PRINTER_ACCESS_ADMINISTER As Integer = &H4
    Public Const PRINTER_ACCESS_USE As Integer = &H8
    Public Const PRINTER_ALL_ACCESS As Integer = STANDARD_RIGHTS_REQUIRED Or
                                                  PRINTER_ACCESS_ADMINISTER Or
                                                  PRINTER_ACCESS_USE

    Public Structure PRINTER_DEFAULTS
        Public pDatatype As IntPtr
        Public pDevMode As IntPtr
        Public DesiredAccess As Integer
    End Structure

    <StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Auto)>
    Public Structure PRINTER_INFO_2
        Public pServerName As String
        Public pPrinterName As String
        Public pShareName As String
        Public pPortName As String
        Public pDriverName As String
        Public pComment As String
        Public pLocation As String
        Public pDevMode As IntPtr
        Public pSepFile As String
        Public pPrintProcessor As String
        Public pDatatype As String
        Public pParameters As String
        Public pSecurityDescriptor As IntPtr
        Public Attributes As Integer
        Public Priority As Integer
        Public DefaultPriority As Integer
        Public StartTime As Integer
        Public UntilTime As Integer
        Public Status As Integer
        Public cJobs As Integer
        Public AveragePPM As Integer
    End Structure

    <DllImport("winspool.drv", CharSet:=CharSet.Auto)>
    Public Function OpenPrinter(pPrinterName As String,
                                 ByRef phPrinter As IntPtr,
                                 ByRef pDefault As PRINTER_DEFAULTS) As Boolean
    End Function

    <DllImport("winspool.drv", CharSet:=CharSet.Auto)>
    Public Function SetPrinter(hPrinter As IntPtr,
                                Level As Integer,
                                ByRef pi2 As PRINTER_INFO_2,
                                command As IntegerAs Boolean
    End Function

    <DllImport("winspool.drv", SetLastError:=True, CharSet:=CharSet.Auto)>
    Public Function GetPrinter(hPrinter As IntPtr,
                                dwLevel As Integer,
                                pPrinter As IntPtr,
                                cbBuf As Integer,
                                ByRef pcbNeeded As IntegerAs Boolean
    End Function

    <DllImport("winspool.drv", SetLastError:=True)>
    Public Function ClosePrinter(hPrinter As IntPtr) As Boolean
    End Function

    <DllImport("kernel32.dll")>
    Public Function GlobalFree(hMem As IntPtr) As IntPtr
    End Function
    <DllImport("kernel32.dll")>
    Public Function GlobalLock(hMem As IntPtr) As IntPtr
    End Function
    <DllImport("kernel32.dll")>
    Public Function GlobalUnlock(hMem As IntPtr) As Boolean
    End Function

End Module
投稿者 KOZ  (社会人) 投稿日時 2022/2/20 21:32:41
DEVMODEW 構造体
https://docs.microsoft.com/ja-jp/windows-hardware/drivers/display/the-devmodew-structure

DEVMODEW structure (wingdi.h)
https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-devmodew

DEVMODE構造体にはパブリックメンバーとプライベートメンバーがあり、用紙サイズは dmPaperSize という定義済みメンバなので設定できると思いますよ。
投稿者 るきお  (社会人) 投稿日時 2022/2/20 21:34:48
おお。用紙サイズ変わりました。いいですね。

Windows 10の「Windows で通常使うプリンターを管理する」をオフにして、「管理者として実行」する必要はありました。
投稿者 まこ  (社会人) 投稿日時 2022/2/20 22:28:58
おお、すごい!!

こちらでも用紙サイズ変更できました。同じ要領で向きも変更できました。
KOZ様、ありがとうございます。