Search code examples
pythonprintingpywin32

How to arrange return content in pywin32


I have few lines of code to send job to my printer using pywin32 but when i send this command the content for the new line it doesn't appear on it row on the paper (newline) after printing but continue to follows the first row the content. When i print this to the terminal it print like what i desire but not when i send the job to the printer. image description of the printed result

Have been searching on this site on how am going to arrange my content when sending it printed but to no avail.

import win32con
import win32print
import win32ui


def text():
    rows = (("PETER PAUL", "MALE", "100000"), ("MARGARET ", "FEMALE", "1000"), ("MICHAEL JORDAN", "MALE", "1"),("AGNES", "FEMALE", "200"))
    return '\r\n'.join('{:20} {:8} {}'.format(*row) for row in rows)


print(text())


def printer():
    dc = win32ui.CreateDC()
    printername = win32print.GetDefaultPrinter()
    dc.CreatePrinterDC(printername)
    dc.SetMapMode(win32con.MM_TWIPS)
    scale_factor = 20
    dc.StartDoc('Win32print ')
    pen = win32ui.CreatePen(0, int(scale_factor), 0)
    dc.SelectObject(pen)
    font = win32ui.CreateFont({
    "name": "Lucida Console",
    "height": int(scale_factor * 10),
    "weight": 400,
})
    dc.SelectObject(font)
    dc.TextOut(scale_factor * 72, -1 * scale_factor * 72, text())
    dc.EndDoc()


printer()

Solution

  • Note: [GitHub]: mhammond/pywin32 - Python for Windows (pywin32) Extensions doesn't have an official doc (or at least I couldn't find any), so I'll be using the 2nd best thing available: ActiveState (I could only find references for the ancient Python2.4, but generally they are OK)

    [ActiveState.Docs]: PyCDC.TextOut wraps [MS.Docs]: TextOutW function. The function doesn't handle \r\n (and definitely other special chars) like print does (the doc doesn't say anything about this), but instead it just ignores them (it doesn't have the concept of line). That means that in order to achieve print - like functionality, the user is responsible for outputting each line individually (of course at a different Y coordinate - to avoid outputting on top of the previous one).

    To better illustrate the behavior, I created an example (based on your code).

    code.py:

    #!/usr/bin/env python3
    
    import sys
    import time
    import win32ui
    import win32con
    import win32print
    
    
    def get_data_strings():
        rows = (("PETER PAUL", "MALE", "100000"), ("MARGARET ", "FEMALE", "1000"), ("MICHAEL JORDAN", "MALE", "1"),("AGNES", "FEMALE", "200"))
        return ["{:20} {:8} {}".format(*row) for row in rows]
    
    
    def text():
        return "\r\n".join(get_data_strings())
    
    
    def paint_dc(dc, printer_dc, paint_each_string=True):
        scale_factor = 20
        if printer_dc:
            x_y = 100, 0  # TopLeft of the page. In order to move away from the point, X increases to positives, while Y to negatives
            font_scale = 10
            y_direction_scale = -1  # For printers, the Y axis is "reversed"
            y_ellipsis = -100
        else:
            x_y = 100, 150  # TopLeft from wnd's client area
            font_scale = 1
            y_direction_scale = 1
            y_ellipsis = 100
    
        font0 = win32ui.CreateFont(
            {
                "name": "Lucida Console",
                "height": scale_factor * font_scale,
                "weight": 400,
            })
        font1 = win32ui.CreateFont(
            {
                "name": "algerian",
                "height": scale_factor * font_scale,
                "weight": 400,
            })
        fonts = [font0, font1]
        dc.SelectObject(font0)
        dc.SetTextColor(0x0000FF00) # 0BGR
        #dc.SetBkColor(0x000000FF)
        dc.SetBkMode(win32con.TRANSPARENT)
        if paint_each_string:
            for idx, txt in enumerate(get_data_strings()):
                dc.SelectObject(fonts[idx % len(fonts)])
                dc.TextOut(x_y[0], x_y[1] + idx * scale_factor * font_scale * y_direction_scale, txt)
        else:
            dc.TextOut(*x_y, text())
        pen = win32ui.CreatePen(0, 0, 0)
        dc.SelectObject(pen)
        dc.Ellipse((50, y_ellipsis, *x_y))
    
    
    def paint_wnd(wnd, paint_each_string=True):
        dc = wnd.GetWindowDC()
        paint_dc(dc, False, paint_each_string=paint_each_string)
        wnd.ReleaseDC(dc)
    
    
    def paint_prn(printer_name, paint_each_string=True):
        printer_name = printer_name or win32print.GetDefaultPrinter()
        dc = win32ui.CreateDC()
        dc.CreatePrinterDC(printer_name)
        dc.SetMapMode(win32con.MM_TWIPS)
        dc.StartDoc("Win32print")
        #dc.StartPage()
        paint_dc(dc, True, paint_each_string=paint_each_string)
        #dc.EndPage()
        dc.EndDoc()
    
    
    def main():
        print("Python {:s} on {:s}\n".format(sys.version, sys.platform))
        print(text())
        time.sleep(0.1)
        if len(sys.argv) > 1:
            if sys.argv[1] == "window":
                paint_func = paint_wnd
                paint_func_dc_arg = win32ui.GetForegroundWindow()
            else:
                paint_func = paint_prn
                paint_func_dc_arg = sys.argv[1]
        else:
            paint_func = paint_prn
            paint_func_dc_arg = None
        paint_func(paint_func_dc_arg, paint_each_string=True)
    
    
    if __name__ == "__main__":
        main()
    

    Notes:

    • I don't have a printer attached (actually I do, but I don't want to print smth every time I run the program), so I'm using current window (cmd) HDC to output data (therefore, I removed the printer specific code)
    • I structured the code a bit, added functions so it's modular
    • I split your text functionality in 2:
      • get_data_strings - which returns a list of strings where each is the textual representation of one row from rows (It would be nicer to make it a generator, but I don't want to overcomplicate things)
      • text - that simply joins them (to be consistent with existing interface)
    • Regarding graphics (GDI):
      • TextOut doesn't care about the pen, it only uses the selected font, background color (dc.SetBkColor), and text color (dc.SetTextColor), but I left it there and drew an ellipse (just for fun)
      • The integer arguments (based on scale_factor) are way out of line (too big - at least for my HDC), so I reduced them to more decent values
      • As you can see, I'm outputting each string individually (and also increment its Y by scale_factor - which it's also the font height). I also kept the old way (printing the whole string) there, you just need to set print_each_string argument to False to achieve the same result that you did
    • time.sleep is required there because outputting to HDC happens a lot faster than print (because of buffering), so even if according to code it happens after print, actually its effect happens before, and print "pushes" the window content (including our graphic output) up, so when the graphic output will go outside the visible area, it will be invalidated and that zone will be repainted, making it disappear.
      I'm not sure if I made myself clear, but once you'll play with the code (comment the line), you'll see what I mean
    • Some of the stuff from code might not work (or work differently) with the printer, as it's a different type of device
    • There's an alternative: using [ActiveState.Docs]: PyCDC.DrawText (wrapper over [MS.Docs]: DrawText function), which is able to deal with multiline strings, but you'd still need to do some calculations in order to adjust the drawing RECT (I didn't feel like playing with that function too)

    Output:

    Program Output

    @EDIT0:

    Added some printer specific functionality. Also, changed the behavior:

    • Without arguments, the app prints to the default printer
    • The 1st argument (if given) can be a printer name or "window" (for initial behavior)
    • The way HDCs work is different:
      • For printers the scaling is much higher (~10 times) - I assume it's because the window HDC works directly with pixels, while for printer HDC also takes DPI into account
      • Also, going top->bottom the Y coordinates increase in absolute values, but are negative
      • I put some values that work OK for "Microsoft Print to PDF" printer, but I assume that those values should be set accordingly by reading printer properties

    Output:

    Img1

    @EDIT1:

    • Added "multiple font support" as requested in a comment