Last active
September 29, 2024 15:32
-
-
Save 59de44955ebd/ed6e721c3350fe16a97dbcfb4c9378bf to your computer and use it in GitHub Desktop.
Minimal demo for native printing (with page setup and print dialog) with Python+Tk in Windows
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from tkinter import Tk, messagebox, scrolledtext, Menu, END, BOTH | |
from ctypes import windll, byref, sizeof, Structure, POINTER | |
from ctypes.wintypes import (BOOL, DWORD, HDC, HGLOBAL, HINSTANCE, HWND, | |
INT, LONG, LPARAM, LPCWSTR, LPVOID, POINT, RECT, UINT, WORD) | |
######################################## | |
# Used constants | |
######################################## | |
DT_CALCRECT = 1024 | |
LOGPIXELSX = 88 | |
LOGPIXELSY = 90 | |
PD_NOSELECTION = 4 | |
PD_PAGENUMS = 2 | |
PD_RETURNDC = 256 | |
PD_USEDEVMODECOPIES = 262144 | |
PSD_DISABLEORIENTATION = 256 | |
PSD_INHUNDREDTHSOFMILLIMETERS = 8 | |
PSD_MARGINS = 2 | |
INCHES_PER_UNIT = 1 / 2540 # inch is 25.4 mm, our unit is hundredth of mm | |
######################################## | |
# Used structures | |
######################################## | |
class DOCINFOW(Structure): | |
def __init__(self, *args, **kwargs): | |
super(DOCINFOW, self).__init__(*args, **kwargs) | |
self.cbSize = sizeof(self) | |
_fields_ = [ | |
('cbSize', INT), | |
('lpszDocName', LPCWSTR), | |
('lpszOutput', LPCWSTR), | |
('lpszDatatype', LPCWSTR), | |
('fwType', DWORD), | |
] | |
class PAGESETUPDLGW(Structure): | |
def __init__(self, *args, **kwargs): | |
super(PAGESETUPDLGW, self).__init__(*args, **kwargs) | |
self.lStructSize = sizeof(self) | |
_fields_ = [ | |
('lStructSize', DWORD), | |
('hwndOwner', HWND), | |
('hDevMode', HGLOBAL), | |
('hDevNames', HGLOBAL), | |
('Flags', DWORD), | |
('ptPaperSize', POINT), | |
('rtMinMargin', RECT), | |
('rtMargin', RECT), | |
('hInstance', HINSTANCE), | |
('lCustData', LPARAM), | |
('lpfnPageSetupHook', LPVOID), # LPPAGESETUPHOOK, unused | |
('lpfnPagePaintHook', LPVOID), # LPPAGEPAINTHOOK, unused | |
('lpPageSetupTemplateName', LPCWSTR), | |
('hPageSetupTemplate', HGLOBAL), | |
] | |
class PRINTDLGW(Structure): | |
def __init__(self, *args, **kwargs): | |
super(PRINTDLGW, self).__init__(*args, **kwargs) | |
self.lStructSize = sizeof(self) | |
_fields_ = [ | |
('lStructSize', DWORD), | |
('hwndOwner', HWND), | |
('hDevMode', HGLOBAL), | |
('hDevNames', HGLOBAL), | |
('hDC', HDC), | |
('Flags', DWORD), | |
('nFromPage', WORD), | |
('nToPage', WORD), | |
('nMinPage', WORD), | |
('nMaxPage', WORD), | |
('nCopies', WORD), | |
('hInstance', HINSTANCE), | |
('lCustData', LPARAM), | |
('lpfnPrintHook', LPVOID), # LPPRINTHOOKPROC, unused | |
('lpfnSetupHook', LPVOID), # LPSETUPHOOKPROC, unused | |
('lpPrintTemplateName', LPCWSTR), | |
('lpSetupTemplateName', LPCWSTR), | |
('hPrintTemplate', HGLOBAL), | |
('hSetupTemplate', HGLOBAL), | |
] | |
######################################## | |
# Used winapi functions | |
######################################## | |
comdlg32 = windll.comdlg32 | |
comdlg32.PageSetupDlgW.argtypes = (POINTER(PAGESETUPDLGW),) | |
comdlg32.PrintDlgW.argtypes = (POINTER(PRINTDLGW),) | |
gdi32 = windll.Gdi32 | |
gdi32.GetDeviceCaps.argtypes = (HDC, INT) | |
gdi32.StartDocW.argtypes = (HDC, POINTER(DOCINFOW)) | |
gdi32.EndDoc.argtypes = (HDC,) | |
gdi32.AbortDoc.argtypes = (HDC,) | |
gdi32.StartPage.argtypes = (HDC,) | |
gdi32.EndPage.argtypes = (HDC,) | |
user32 = windll.user32 | |
user32.DrawTextW.argtypes = (HDC, LPCWSTR, INT, POINTER(RECT), UINT) | |
class Main(Tk): | |
def __init__(self): | |
super().__init__() | |
self.title('Tk Print Demo') | |
# defaults, in mm/100 | |
self.print_paper_size = POINT(21000, 29700) # Din A4 (for Europe) | |
self.print_margins = RECT(1000, 1500, 1000, 1500) | |
menu_bar = Menu(self) | |
self.config(menu=menu_bar) | |
file_menu = Menu(menu_bar, tearoff=False) | |
menu_bar.add_cascade(label='File', menu=file_menu) | |
file_menu.add_command(label='Page Setup...', command=self.page_setup) | |
file_menu.add_command(label='Print...', command=self.do_print) | |
file_menu.add_separator() | |
file_menu.add_command(label='Exit', command=self.destroy) | |
help_menu = Menu(menu_bar, tearoff=False) | |
menu_bar.add_cascade(label='Help', menu=help_menu) | |
help_menu.add_command(label='About', command=self.about) | |
self.columnconfigure(0, weight=1) | |
self.rowconfigure(0, weight=1) | |
self.text_widget = scrolledtext.ScrolledText(self) | |
self.text_widget.pack(fill=BOTH, expand=True) | |
for i in range(301): | |
self.text_widget.insert(END, | |
f'{i:>3} Hello World! Hello World! Hello World! Hello World! Hello World!\n') | |
def about(self): | |
messagebox.showinfo('About', 'A minimal Tk print demo for Windows') | |
def page_setup(self): | |
psd = PAGESETUPDLGW() | |
psd.hwndOwner = int(self.frame(), 16) | |
psd.Flags = PSD_INHUNDREDTHSOFMILLIMETERS | PSD_MARGINS | PSD_DISABLEORIENTATION | |
psd.ptPaperSize = self.print_paper_size | |
psd.rtMargin = self.print_margins | |
if comdlg32.PageSetupDlgW(byref(psd)): | |
self.print_paper_size = psd.ptPaperSize | |
self.print_margins = psd.rtMargin | |
def do_print(self, doc_name='Document'): | |
pdlg = PRINTDLGW() | |
pdlg.Flags = PD_RETURNDC | PD_USEDEVMODECOPIES | PD_NOSELECTION | |
pdlg.nFromPage = 1 | |
pdlg.nToPage = 1 | |
pdlg.nMinPage = 1 | |
pdlg.nMaxPage = 0xffff | |
if not comdlg32.PrintDlgW(byref(pdlg)): | |
return False | |
hdc = pdlg.hDC | |
di = DOCINFOW() | |
di.lpszDocName = doc_name | |
job_id = gdi32.StartDocW(hdc, byref(di)) | |
if job_id <= 0: | |
return False | |
# Get printer resolution | |
pt_dpi = POINT() | |
pt_dpi.x = gdi32.GetDeviceCaps(hdc, LOGPIXELSX) | |
pt_dpi.y = gdi32.GetDeviceCaps(hdc, LOGPIXELSY) | |
# Set page rect in logical units, rcPage is the rectangle {0, 0, maxX, maxY} where | |
# maxX+1 and maxY+1 are the number of physically printable pixels in x and y. | |
# rc is the rectangle to render the text in (which will, of course, fit within the | |
# rectangle defined by rcPage). | |
rcPage = RECT() | |
rcPage.top = 0 | |
rcPage.left = 0 | |
rcPage.right = round(self.print_paper_size.x * pt_dpi.x * INCHES_PER_UNIT) | |
rcPage.bottom = round(self.print_paper_size.y * pt_dpi.x * INCHES_PER_UNIT) | |
rc = RECT() | |
rc.left = round(self.print_margins.left * pt_dpi.x * INCHES_PER_UNIT) | |
rc.top = round(self.print_margins.top * pt_dpi.x * INCHES_PER_UNIT) | |
rc.right = rcPage.right - round(self.print_margins.right * pt_dpi.x * INCHES_PER_UNIT) | |
rc.bottom = rcPage.bottom - round(self.print_margins.bottom * pt_dpi.x * INCHES_PER_UNIT) | |
# calculate line height (quick & dirty) | |
success = user32.DrawTextW(hdc, 'Whatever', -1, byref(rc), | |
DT_CALCRECT) > 0 | |
if not success: | |
return False | |
line_height = rc.bottom - rc.top | |
rc.right = rcPage.right - round(self.print_margins.right * pt_dpi.x * INCHES_PER_UNIT) | |
rc.bottom = rcPage.bottom - round(self.print_margins.bottom * pt_dpi.x * INCHES_PER_UNIT) | |
lines_per_page = (rc.bottom - rc.top) // line_height | |
lines = self.text_widget.get('1.0', END).split('\n') | |
if pdlg.Flags & PD_PAGENUMS: | |
first_page, last_page = pdlg.nFromPage, pdlg.nToPage | |
else: | |
first_page, last_page = 1, 0xffffffff | |
# print successive pages | |
page = 0 | |
for i in range(0, len(lines), lines_per_page): | |
page += 1 | |
if page < first_page: | |
continue | |
success = gdi32.StartPage(hdc) > 0 | |
if not success: | |
break | |
success = user32.DrawTextW(hdc, '\n'.join(lines[i:i + lines_per_page]), | |
-1, byref(rc), 0) > 0 | |
if not success: | |
break | |
gdi32.EndPage(hdc) | |
if page >= last_page: | |
break | |
if success: | |
gdi32.EndDoc(hdc) | |
else: | |
gdi32.AbortDoc(hdc) | |
return success | |
if __name__ == '__main__': | |
main = Main() | |
main.mainloop() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Screenshot (Python 3.12/Windows 11/German)
