PySimpleGUI / PySimpleGUI

Python GUIs for Humans! PySimpleGUI is the top-rated Python application development environment. Launched in 2018 and actively developed, maintained, and supported in 2024. Transforms tkinter, Qt, WxPython, and Remi into a simple, intuitive, and fun experience for both hobbyists and expert users.
https://www.PySimpleGUI.com
Other
13.26k stars 1.84k forks source link

[Question]Wrap position for unicode word/char poor #2356

Open jason990420 opened 4 years ago

jason990420 commented 4 years ago

Type of Issues (Enhancement, Error, Bug, Question)

I got bad wrap position for Text output, and found nothing I can change it. It seems Text element is tk.Label and there's only one option 'wraplength' which just change the width of text ouput. The samething found in Multiline.

For English, words seperated by space, but not for some other languages. You can find following output. Left side is for spaces in text and right side is for space removed.

In tkinter, Text widget can set the wrap=char to avoid this situation. I don't find same or similar Text Widget in PySimpleGUI. What can I do if all spaces kept ?

image

Operating System

Windows 10

Python version

Python 3.7.2

PySimpleGUI Port and Version

PySimpleGUI '4.6.0 Released 11-Nov-2019'

Your Experience Levels In Months or Years

1____ Python programming experience 20__ Programming experience overall ___Tkinter____ Have used another Python GUI Framework (tkiner, Qt, etc) previously (yes/no is fine)?

You have completed these steps:

Code or partial code causing the problem

import PySimpleGUI as sg

font = 'Times 16'

txt1 = '''1回のフライトをgoogle購入すると、初めgoogle ての方は経験豊富なイン
ストラク google ターとの 1 対 1 による飛行を google 体験出来ます。4歳以上の方
には 1 人 1 人に合ったフライトを提案し、必要な物も全てご提供いたします。も
う少し本格的に習いたいという方には、合計10分間のスポーツgoogleスクールを
受講し google ていただけます。スクールを終えた後はプロフライヤーとして必要な
google スキルを習得していき、優雅google に飛行出来るようになる環境も整えてお
り google ます。場合によっては屋内スカイダイビングの大会への参加も可能になり
ます。'''

txt2 = a.replace('\n', '').replace(' ','')

layout = [[sg.Text(txt1, size=(30, 20), font=font, key='Text1'),
           sg.Text(txt2, size=(30, 20), font=font, key='Text2')]]

window = sg.Window('Test', layout=layout, finalize=True)

window['Text1'].Widget.configure(wraplength=400)
window['Text2'].Widget.configure(wraplength=400)

while True:

    event, values = window.read()

    if event == None:
        break

window.close()
PySimpleGUI commented 4 years ago

You are correct that PySimpleGUI uses tk.Label not tk.Text for the single line text.

However, for the Multiline element, the ScrolledText widget is used. This Widget is a special subclassed version of the Text widget. Thus, the Multiline Element is a Text widget and can be configured as one.

This sample code modifies the setting you mentioned wrap. I'm not exactly sure which setting you needed nor which text to output so I made them options and buttons. It's the most options PySimpleGUI can offer at this time.

import PySimpleGUI as sg

font = 'Times 16'

txt1 = '''1回のフライトをgoogle購入すると、初めgoogle ての方は経験豊富なイン
ストラク google ターとの 1 対 1 による飛行を google 体験出来ます。4歳以上の方
には 1 人 1 人に合ったフライトを提案し、必要な物も全てご提供いたします。も
う少し本格的に習いたいという方には、合計10分間のスポーツgoogleスクールを
受講し google ていただけます。スクールを終えた後はプロフライヤーとして必要な
google スキルを習得していき、優雅google に飛行出来るようになる環境も整えてお
り google ます。場合によっては屋内スカイダイビングの大会への参加も可能になり
ます。'''

txt2 = txt1.replace('\n', '').replace(' ','')

layout = [[sg.Text(txt1, size=(30, 20), font=font, key='Text1'),
           sg.Text(txt2, size=(30, 20), font=font, key='Text2'),
           sg.Multiline(size=(30,20), key='-MLINE-', font=font)],
            [sg.R('1',0, default=True), sg.R('2',0)],
           [sg.B('char'), sg.B('word'), sg.B('none')]]

window = sg.Window('Test', layout=layout, finalize=True)

window['Text1'].Widget.configure(wraplength=400)
window['Text2'].Widget.configure(wraplength=400)

while True:
    event, values = window.read()
    if event == None:
        break
    wrap_arg = event
    text = txt1 if values[0] else txt2
    window['-MLINE-'].Widget.configure(wrap=wrap_arg)             # Configure the tk.Text widget - Choices are "char", "word", None
    window['-MLINE-'].update(text)
window.close()
jason990420 commented 4 years ago

After revised sg.Text to sg.Multiline and with configure wrap option 'char', it is good now. But, everthing is not good enough. For sg.Text, the text is splitted by space char, but I can set height of size to None to auto adjust the height for text length.

image

sg.Multiline, for me good: the text is splitted by char bad:

  1. Height of size to None not auto adjusted the height for text
  2. There's a scrollbar which cannot be set invisiable
  3. text can be edited

image

Note: calculated the text height, but found not exact, sometime one line more, sometime one line less....It maybe myself problem.

txt = tkFont.Font(family="Times New Roman", size=14)
width = txt.measure("What the heck?")

Additional, I add bind mouse wheel to sg.Column. Column scrolled when scroll Combo. Sometime scroll Coulumn, cannot scroll to bottom of Column. I need to check why, not question here, just for reference.

You can find the source code here: sg.Text sg.Multiline newsapi key

PySimpleGUI commented 4 years ago

None is never a good parameter for a size.

PySimpleGUI commented 4 years ago

I'm sorry that "everything is not good enough". Issues I find the most helpful are very specific requests or bug reports about a specific item.

I don't have the ability to go through your source code and examine it in detail to discover the problems you're having with how you're using PySimpleGUI and also try to find the solutions for you. If the problem you're trying to solve is beyond the capabilities of PySimpleGUI, then an enhancement request is the best way to go about solving the problem.

It sounds like you need an entirely new type of Text element that uses the tk.Text widget instead of the tk.Label widget that's used today. If so that's not a trivial addition to the package. Maybe Qt is a better solution for you to examine in this situation?

PySimpleGUI commented 4 years ago

You don't happen to be the wizard that's been creating the amazing games using PySimpleGUI by any chance?

I just saw your Solitaire game and I'm speechless. It's amazing what you're able to achieve with PySimpleGUI code.

Can you change it to use these BASE64 cards? You can add this code to the top of the file and you'll have a single file that contains everything.

cards.py.txt

PySimpleGUI commented 4 years ago

I did a similar change to your Minesweeper game. It works well in these games where the graphics are minimal.

https://pysimplegui.trinket.io/demo-programs#/games/minesweeper

Take a look a the changes I made to your code. I explained how to use the new bind method and using the Base64 images.

jason990420 commented 4 years ago

Don't understand why upload 'Klondike_Solitaire_BASE64.py' to google cloud always failed, but 'Klondike_Solitaire_BASE64.rar' pass. I just try to use it to design some softwares, so it should come to understand it quickly. Anyway you can find update file as Klondike_Solitaire_BASE64.rar

PySimpleGUI commented 4 years ago

I share code via GitHub or DropBox. I never use Google Drive so I don't know the answer, sorry.

Thank you very much for the FAST change!!! Wow... just wow.

The original file said it has 545 lines of code, but when all blank lines and comments are removed, it's only 400 lines.

BTW, your comparisons to None should always use is and not ==

bad

if pp.up == None: break

better

if pp.up is None: break

best

if pp.up is None:
    break

I do understand the desire to compact the code though and so I'm not totally against a single-line if statement.

I still find it difficult to believe that the program is written using PySimpleGUI.... especially when I see the dealing animation AND when I double-clicked a card like in the real game.

You're quite a skilled software engineer.

jason990420 commented 4 years ago

Method bind checked and work fine ! Will keep going to find solution about wrap issue. Thanks !!!

PySimpleGUI commented 4 years ago

Question on your Solitaire game. This line of code is stopped the ability to run on non-Windows platforms:

ctypes.windll.user32.SetProcessDPIAware()   # Set unit of GUI to pixels

I removed the line and it seems to have no impact and as a result I was able to run it on Trinket.

PySimpleGUI commented 4 years ago

@jason990420 would it be OK to turn your games into Demo Programs? I KNOW kids will find them exciting to see and play with.

I will be happy to add whatever information to the top of the programs as you would like.... links to your personal site, email address if you want, etc. I've got no problem give you the full credit you deserve.

I appreciate the effort you've put into them. They have also made PySimpleGUI better as I clearly saw the need to add the Element.bind method so that your Minesweeper application doesn't get direct callbacks from tkinter.

After looking at other examples of Minesweeper shown on the Wikipedia page, yesterday I also added the ability to set the "disabled text color" for buttons, both at creation time and when you update the button.

This allowed me to color the text depending on the number on the button.

To run this you'll need the PySimpleGUI.py file from the GitHub (version 4.11.0.1)

Minesweeper_Colored_Numbers.py.txt

image

BTW - have you tried to upload your .py file as a .txt file to Google Drive? I think they are maybe complaining because it's code

jason990420 commented 4 years ago

All my codes are open, anything is OK, it's my honor.

Unit of 'point' is generally set to point, not pixel and not real point. Most of time, it's 72 point/inch. Without ctypes.windll.user32.SetProcessDPIAware(), everything will be enlarged. at least for my Win10 system or, not sure, for Python. I didn't think about the color of text in MineSweeper because I am color blindness. Most of time, I hate game or software which use color to identify something.

Here, I got something different for wrap, just do wrap by myself ! Now it work fine ! image

...
s = FONT.Font(family='Helvetica', size=12, weight='bold')
def split(txt):
    # Split text for space, ASCII string, non-Unicode char into list
    txt = txt.strip()
    if txt is '':
        return []
    result = []
    string = ''
    for i in range(len(txt)):
        if txt[i] in [' ', '\n','\r']:
            if string is not '':
                result.append(string)
                result.append(' ')
            string = ''
        elif txt[i] in ASCII:
            string += txt[i]
        else:
            if string is not '':
                result.append(string)
            result.append(txt[i])
            string = ''
    if string != '':
        result.append(string)
    return result

def wrap(txt, dist):
    # Wrap string by add '\n' into string for pixel width limit
    if txt is '':
        return '', 1
    tmp = split(txt)
    old_string = ''
    string = ''
    result = ''
    length = len(tmp)
    lines = 0
    for i in range(length):
        string += tmp[i]
        if s.measure(string) > dist:
            result += old_string + '\n'
            lines += 1
            if tmp[i] is ' ':
                string = old_string = ''
            else:
                string = old_string = tmp[i]
        else:
            old_string = string
    result += old_string + '\n'
    lines += 1
    return result, lines
...

There's another three issues left for me to solve:

  1. Blind color for Combo only when 1st time shown the default value, it should my problem because I test it in small script it is ok. I am sure it is the color problem, not nothing shown. After selected, it is good . image

  2. Mouse wheel scroll on Combo which not in Column layout, but also scroll the Column. GIF

  3. Some content not shown, or just part of them, when scroll down to botton. image

Source file in key.py, my_api_key = 'XXXXXXXXXXXXXXXXXXXXX' you can get your key in https://newsapi.org/docs/authentication

PySimpleGUI commented 4 years ago

I am confused about #1. Is it a problem?

Thank you for taking the time to write it up. Some are duplicates but it'll be good to have your additional info. I think the scroll to the very bottom was raised previously. The double scroll may be as well.

Are changes needed to the wrap or do you have that completely figured out? I can ignore the whole thing, everything is fine the way it was?

I removed the ctypes stuff entirely and it's running fine on Windows and Linux. There are a lot of Linux users that will love this.

I'm STILL blown away by the solitaire. Did you have to go around anything or is it pure PySimpleGUI code with no reaching directly into tkinter?

PySimpleGUI commented 4 years ago

For #1 For combo boxes, if an initial value is not set, it will be completely blank. I don't know if that has anything to do with what you're reporting. Please post sample code.

PySimpleGUI commented 4 years ago

Oh! If you post on Reddit, this the Solitaire would be a big hit.

jason990420 commented 4 years ago

Script is under the original post. Combo box set the default_value, but blank for some themes, will be unclear for some theme. For poker game, there're only two statements used, it may come from tkinter, not sure they can found in PySimpleGUI.

  1. 'element'.TKCanvas.update_idletasks()
  2. 'element'.TKCanvas.find_overlapping(x,y,x,y)

Reddit? not sure ? just post on https://learnku.com/python

PySimpleGUI commented 4 years ago

update_idletasks is usually done on the window. For that you simply call window.refresh(). So that one is easy.

The other one is definitely not.

Did you get a lot of interest in your post?

jason990420 commented 4 years ago

No, it seems not so many people over https://learnku.com/python. Just to find a place without much limit for post.

PySimpleGUI commented 4 years ago

If you setup your GitHub then I can direct people there to get your software.

I have posted your 2 games to a folder here on the GitHub, but before doing any kind of announcements or posting about them on Reddit, I would like to get the "credits" at the top worked out so that you get full credit and that people have a link to go to find out about your work.

Here's the folder that currently has your Solitaire in it https://github.com/PySimpleGUI/PySimpleGUI/tree/master/UserCreatedPrograms

I would like release the Minesweeper, both colored numbers and white colored numbers there too. To do so I need to document and release the new ability to set the disabled button text color, a feature that has been requested in the past.

I would also like to work out the ttk buttons. I have been unable to get Minesweeper to work correctly when ttk buttons are used. The size of the squares is incorrect (tiny tiny). Here's what the board looks like when I force using ttk buttons.

image

Thanks for taking the time to make these games. I KNOW kids will be much more excited (and adults) to take a look at PySimpleGUI after seeing them.

PySimpleGUI commented 4 years ago

About the combo box problem.

I spent quite a lot of time going through and pulling apart your program so that I could isolate the problem. I normally don't have the time/ability to go through an entire 320 line program and pull out the bits and pieces in order to make a small test case.

I noticed the logic of the update required setting of disabled to False in order to change the readonly state. I've changed the update logic so that you can change just the readonly parameter. Note that you will lose disabled if changing the readonly state as there is a single "State" for a combox in tkinter.

It seems your problem is the color scheme.

If you use another look and feel, you'll see if works ok

I'm guessing that your colors match the disabled gray value that tkinter uses.

When you perform a read on the window, you'll see that even though it looks like there's nothing there, a value is returned.

When I style a combobox, these are the settings I set:

                    combostyle.configure(style_name, foreground=element.TextColor)
                    combostyle.configure(style_name, selectbackground=element.BackgroundColor)
                    combostyle.configure(style_name, fieldbackground=element.BackgroundColor)
                    combostyle.configure(style_name, selectforeground=element.TextColor)

Here's the test code that demonstrates your look and feel is causing the problem in this one. It's unclear if other published look and feels have this trouble. I have not seen it until you reported it.

import PySimpleGUI as sg

sg.LOOK_AND_FEEL_TABLE['News'] = {'BACKGROUND': '#004000', 'TEXT': '#ffffff',
    'INPUT': '#008000', 'TEXT_INPUT': '#e0e0e0', 'SCROLL': '#e0e0e0e',
    'BUTTON': ('#ffffff', '#000080'), 'PROGRESS': ('#004000', '#00ff00'),
    'BORDER': 1, 'SLIDER_DEPTH': 0, 'PROGRESS_DEPTH': 0, 'COLOR_LIST':
    ['#004000', '#004000', '#004000', '#004000'], 'DESCRIPTION': ['Turquoise',
    'Red', 'Yellow']}

sg.change_look_and_feel('Light Blue 4')
sg.change_look_and_feel('News')           # uncomment to see the problem of value "disappearing"

layout = [  [sg.Text('My Window')],
            [sg.Combo([1,2,3,4], size=(4,1), readonly=True, default_value=3, key='C'), sg.Text(size=(12,1), key='-OUT-')],
            [sg.T('Press GO to see your default is 3 when values are printed')],
            [sg.Button('Go'), sg.Button('Exit')]  ]

window = sg.Window('Window Title', layout, finalize=True)

while True:             # Event Loop
    event, values = window.read()

    print(event, values)
    if event in (None, 'Exit'):
        break

window.close()
jason990420 commented 4 years ago

Just do it ! Not sure when I will spend time to figure out how to setup Github.

At the time now, will not to use Column, but Graph for my News project. At least, Scroll issue should be no problem when using Graph, just have to spend much time for scroll event handler.

Let me explain the scroll issue here.

  1. window['Column'].Widget.bind_all() . Column scroll . All elements scroll
  2. window['Column'].Widget.bind() . All elements won't scroll . Column no scroll because other Text and Image on the top of Column. . get some space on Column, no binding necessary, and it only scroll on space. . I found notes on source code, it say 'do not use yet! not working'
PySimpleGUI commented 4 years ago

Scrolling of stuff is a known and continued problem for me.

I'll work on it again. Maybe I'll get lucky like I did with the ttk widgets and have a big breakthrough.

There are a few places where scrolling works great, and a few where it doesn't. I struggle when there's a frame with a canvas with a scrollbar with a ? with a frame that's supposed to scroll only the ? (I'm making up the widgets... .the point is that it's very confusing to me when multiple widgets are combined together).

PySimpleGUI commented 4 years ago

@jason990420 By the way, you do not need to run your event loops with a timeout. Both the Minesweeper and the Solitaire programs run fine with no timeout. The reason for not running a timeout is a huge saving of CPU time.

A timeout is essentially polling or timing something. Everything you're using inside your event loop is generating events. Mouse movements, dragging, etc, all generate events without the need for a timeout.

If there's something I'm missing, please let me know what it is.

PySimpleGUI commented 4 years ago

@jason990420 can you send me your contact information at PySimpleGUI@PySimpleGUI.org?

I noticed in a blog entry the mention that PySimpleGUI is licensed under an MIT License. It is NOT MIT licensed. It is LGPL3 licensed. It would be great if you could modify the blog entry so that readers don't get the wrong impression.

The license info is listed in the license.txt file on this GitHub: https://github.com/PySimpleGUI/PySimpleGUI/blob/master/license.txt

jason990420 commented 4 years ago

Just to mean All my files are completely free and open source under the permissive MIT license. No strings attached, no royalties, nothing. I didn't say it is for PySimpleGUI. Of course, PySimpleGUI is LGPL3 licensed.

Where's my contact information at PySimpleGUI@PySimpleGUI.org ? I don't know. For the email, I won't check it everyday. Recently, I checked PySimpleGUI.org almost everyday.

PySimpleGUI commented 4 years ago

Ohhhhhhh! I'm sorry! The translation was bad. It came across, in the translation (to me) that it was PySimpleGUI being discussed in the license part. Sorry for the confusion! Thank you for all the stuff you've been doing! I posted your 2 games on Reddit over the weekend. They're very impressive and I know will be a great motivator for other people to learn from.

I want to study how you did the card movements in Solitaire. How did you know what was clicked on to move the stacks or the cards. When the cards are all stacked up, it's only that slim part of the bottom-most card that can be moved. Much more to learn from you that I can pass one.

I don't have a demo showing how to create a rectangle and then drag it around. This is what I need to make for people.

jason990420 commented 4 years ago
def get_card(x, y, down=True):
    # get card by position (x,y), button down for 1st one, button up for 2nd one
    x, y = draw._convert_canvas_xy_to_xy(x, y)
    ids = draw.TKCanvas.find_overlapping(x,y,x,y)
    if down+len(ids)<2: return None
    return id_to_card(ids[down-2])

For PySimpleGUI, there's no way to retrieve objects from the coordinates, so I used Canvas's method find_overlapping () in Tkinter. It will return a list of all the objects at coordinate point, and the objects in list are arranged from the bottom to top. Of course, the bottom one is the rectangle image. The coordinates defined in Tkinter are different from it in PySimpleGUI, so I used method _convert_canvas_xy_to_xy () found in the source code of PySimple for coordinate conversion.

For how to find other cards on top of the card which has been found, the no. of upper card can be read from information of each card, then we can find all the cards one by one.

PySimpleGUI commented 4 years ago

Ahhhhhhhhhhhhhhhhh

I didn't see you were accessing anything directly in tkinter.

I'll look at what it'll take to extend the API again to accommodate access to this.

PySimpleGUI's features have often been guided by direct and immediate needs of user code. It's how some of the best features have been completed. Your right click for buttons is an example. I didn't make specifying the right click info it the most portable across the PySimpleGUI ports, but I did make it fit the PySimpleGUI event model.

Let me look at how to do this based on what you did and tkinter's calls. I haven't done anything with these drawing figures previously in tkinter so I didn't know to even look at this function.

Thank you!

PySimpleGUI commented 4 years ago

@jason990420 I have modified the Solitaire game to use the latest Graph method and replaced the calls to update_idle_tasks so that only PySimpleGUI calls are in the code. The update was replaced by call to window.refresh which is what is needed if you don't want to call read and instead just want to update the window's contents.

The new Graph method get_figures_at_location replaced the call to tkinter's find_overlapping.

I think I've finally got both of your program's requirements handled using plain PySimpleGUI calls.

jason990420 commented 4 years ago

Good job !!! Should I close this question ? or keep it going ...

PySimpleGUI commented 4 years ago

LOL.... All I did was follow your lead of what you needed to make these games.

PySimpleGUI evolves as user demand evolves. When there are missing features that make a lot of sense and open up new capabilities for everyone, and it can be done in a simple manner, then they get rolled into the package.

Your solitaire game pushed the library to enable the ability to grab and move stuff around on a canvas, a critical feature to so many drawing applications. I'm sure people are going to find this to be a fantastic capability.

Dragging with PySimpleGUI

jason990420 commented 4 years ago

Good !

PySimpleGUI commented 4 years ago

I noticed in a recent post you needed to import the tkinter FONT method. I was a little unsure as to why. Is it to get the measurements of your text for wrapping?

I'm always carefully watching for capabilities that are lacking from ALL ports of PySimpleGUI, not just the tkinter ones. The changes I recently made will apply equally well to PySimpleGUIQt and PySimpleGUIWeb was they did the tkinter PySimpleGUI.

jason990420 commented 4 years ago

In sg.Text, it seems text wrap only work well for ASCII code, it always think conitinuous chars of Unicode as a word, then wrap text at where space char. For following example, you can find the difference. It seems there still problem for my converter, just forget it. Three issues here:

  1. wrap method cannot be defined for sg.Text
  2. I cannot get how many lines will be
  3. There'are two units used, line width in chars and window size in pixel or point. Manual ajustment is always required for text display, and there's no way or function to exactly convert between those two features. The samething happened on height of text.

That's why I calculate the width by myself. Even, to calculate the width of string by using font.measure, there still problem for ASCII and Uncode.

import PySimpleGUI as sg
from tkinter import font as FONT

def split(txt):
    # Split text for space, ASCII string, non-Unicode char into list
    txt = txt.strip()
    if txt is '':
        return []
    result = []
    string = ''
    for i in range(len(txt)):
        if txt[i] in [' ', '\n','\r']:
            if string is not '':
                result.append(string)
                result.append(' ')
            string = ''
        elif txt[i] in ASCII:
            string += txt[i]
        else:
            if string is not '':
                result.append(string)
            result.append(txt[i])
            string = ''
    if string != '':
        result.append(string)
    return result

def wrap(txt, dist, lines_limit):
    # Wrap string by add '\n' into string for pixel width limit
    if txt is '':
        return '', 1
    tmp = split(txt)
    old_string = ''
    string = ''
    result = ''
    length = len(tmp)
    len_1  = length - 1
    lines = 0
    for i in range(length):
        string += tmp[i]
        if s.measure(string) > dist:
            result += old_string + '\n'
            lines += 1
            if tmp[i] is ' ':
                string = old_string = ''
            else:
                string = old_string = tmp[i]
        else:
            old_string = string
        if lines == lines_limit:
            old_string = ''
            break
    if old_string is not '':
        result += old_string
        lines += 1
    return result

font    = 'courier 16 bold'
location= (200, 200)
size    = (1350, 400)
bg      = 'green'
ASCII   = [chr(i) for i in range(256)]
s       = FONT.Font(family='Segoe', size=16)

text    = "Python是一种广泛使用的解释型, 高级编程, 通用型编程语言, 由吉多·范罗苏姆创造, 第一版发布于1991年. 可以视之为一种改良 ( 加入一些其他编程语言的优点, 如面向对象 ) 的LISP. [来源请求]Python的设计哲学强调代码的可读性和简洁的语法 ( 尤其是使用空格缩进划分代码块, 而非使用大括号或者关键词 ). 相比于C++或Java, Python让开发者能够用更少的代码表达想法. 不管是小型还是大型程序, 该语言都试图让程序的结构清晰明了."
wraptext= wrap(text, 550, 11)
sg.change_look_and_feel('Dark Blue 3')

layout = [[sg.Text(text=text    , size=(40,12), font=font, auto_size_text=False),
           sg.Text(text=wraptext, size=(40,11), font=font, auto_size_text=False)]]

window = sg.Window(
            title='Test',
            layout=layout,
            location=location,
            size=size,
            font=font,
            background_color=bg,
            finalize=True)

while True:

    event, values = window.read()
    print(event, values)

    if event == None:
        break

window.close()

image

jason990420 commented 4 years ago

I create a Text object in Canvas to satisfy myself requirements. All text drawn by Canvas.DrawText(). image

import PySimpleGUI as sg
import ctypes

ctypes.windll.user32.SetProcessDPIAware()   # Set unit of GUI to pixels

alphabet    = [chr(i) for i in range(48, 58)]+[
               chr(i) for i in range(65, 91)]+[
               chr(i) for i in range(97,123)]
ASCII       = [chr(i) for i in range(256)]

class Text():

    def __init__(self, draw):

        self.draw               = draw      # where to draw
        self.data               = {}        # All information

    def clear(self):
        self.bg_color   = ''                # background color
        self.chars              = 0         # line width in char
        self.distance           = []        # pixels to insert into lines
        self.font               = ''        # Font
        self.font_size          = 0         # Font size
        self.height             = 0         # Height of Text area in pixel
        self.ids                = []        # All id drawn
        self.lines              = 0         # Lines after wrapped
        self.pad                = 0         # pixels offset to position
        self.position           = (0,0)     # Left-top position of text
        self.text               = ''        # Text
        self.color              = ''        # Text Color
        self.text_wrap          = []        # Wrapped text
        self.width              = 0         # Line Width in pixel

    def Delete(self, index):
        if index not in self.data:
            return
        for i in self.data[index]['ids']:
            self.draw.DeleteFigure(i)
        del self.data[index]

    def DrawText(self, text, chars=40, pad=6, x=0, y=0,
                color='black', bg_color=None, line_color=None,
                family='Courier', font_size=12,
                bold=False, italic=False, underline=False, overstrike=False):

        # Draw text on canvas
        if text is '':
            return None

        self.clear()

        self.width      = chars*font_size+2*pad
        self.text       = text
        self.chars      = chars
        self.pad        = pad
        self.color      = color
        self.bg_color   = bg_color
        self.position   = (x, y)
        self.font_size  = font_size

        bold            = 'bold '       if bold         else ''
        italic          = 'italic '     if italic       else ''
        underline       = 'underline '  if underline    else ''
        overstrike      = 'overstrike ' if overstrike   else ''
        style           = (bold + italic + underline + overstrike).strip()

        if style is '':
            font = (family, font_size)
        else:
            font = (family, font_size, style)
        self.font = font

        X = x + pad
        Y = y - pad

        self.text_wrap, self.distance, self.lines = self.wrap(
            text, chars, font_size)
        self.height = self.lines*2*self.font_size+2*self.pad

        if self.bg_color is not 'None':
            id_rectangle = self.draw.DrawRectangle((x, y),
                (x+self.width,y-self.height), line_width=1,
                fill_color=bg_color, line_color=line_color)
            self.ids.append(id_rectangle)
        for i in range(self.lines):
            self.DrawTextLine(self.text_wrap[i], X, Y, self.distance[i],
                font, font_size, color, bg_color)
            Y -= font_size*2
        index_dict = 0 if len(self.data)==0 else max(self.data.keys())+1
        self.save_data(index_dict)

        return index_dict

    def DrawTextLine(self, text, x, y, dist, font, font_size, color, bg_color):
        # Draw one text line
        if text == '':
            return

        X    = x
        Y    = y - font_size
        step = dist//len(text) + 1

        for i in range(len(text)):
            offset  = font_size//2 if text[i] in ASCII else font_size
            id_text = self.draw.DrawText(text[i], (X+offset,Y), font=font,
                            text_location='center',color = color)
            self.ids.append(id_text)
            d     = step if dist > 0 else 0
            X    += offset*2+d
            dist -= step

    def length(self, text, font_size):

        # Caluclate width in pixel just by font size
        width = 0
        for i in range(len(text)):
            width += font_size if text[i] in ASCII else 2*font_size
        return width

    def move_delta(self, index, dx, dy):

        # Move object on canvas by delta position
        if index not in self.data:
            return

        for i in self.data[index]['ids']:
            self.draw.MoveFigure(i, dx, dy)

        self.data[index]['position'] = (
            self.data[index]['position'][0]+dx,
            self.data[index]['position'][1]+dy)

    def move(self, index, x, y):
        # Move object on canvas by absolute position
        dx = x - self.data[index]['position'][0]
        dy = y - self.data[index]['position'][1]
        self.move_delta(index, dx, dy)

    def save_data(self, index):

        # Save all data for text draw
        self.data[index] = {}
        self.data[index]['bg_color'        ] = self.bg_color
        self.data[index]['chars'           ] = self.chars
        self.data[index]['distance'        ] = self.distance
        self.data[index]['font'            ] = self.font
        self.data[index]['font_size'       ] = self.font_size
        self.data[index]['height'          ] = self.height
        self.data[index]['ids'             ] = self.ids
        self.data[index]['lines'           ] = self.lines
        self.data[index]['pad'             ] = self.pad
        self.data[index]['position'        ] = self.position
        self.data[index]['text'            ] = self.text
        self.data[index]['color'           ] = self.color
        self.data[index]['text_wrap'       ] = self.text_wrap
        self.data[index]['width'           ] = self.width

    def split(self, text):
        # Consecutive numbers and English letters can be used as one word,
        # and every other letter is considered as one word.
        if text is '':
            return []

        result = []
        string = ''

        for i in range(len(text)):

            if text[i] in alphabet:
                string += text[i]
            else:
                if string != '':
                    result.append(string)
                    string = ''
                result.append(text[i])

        if string != '':
            result.append(string)

        return result

    def wrap(self, text, chars, font_size):

        # wrap text to lines and record the pixels required for right alignment.
        # Get the number of lines of formatted text at the same time.
        words       = self.split(text)
        long        = 0
        temp        = ''
        result      = []
        distance    = []
        width       = chars*font_size

        for word in words:
            l = self.length(word, font_size)

            if word in '\r\n':
                result.append(temp.strip())
                distance.append(0)
                temp = ''
                long = 0
            elif long+l > width:
                temp = temp.strip()
                result.append(temp)
                distance.append(width-self.length(temp, font_size))
                temp = word
                long = l
            else:
                temp += word
                long += l

        if temp != '':
            result.append(temp)
            distance.append(0)

        return result , distance, len(result)

text1 = ("Python(英国发音:/ˈpaɪθən/ 美国发音:/ˈpaɪθɑːn/)是一种广泛使用的"
         "解释型、高级编程、通用型编程语言,由吉多·范罗苏姆创造,第一版发布于"
         "1991年。\n可以视之为一种改良(加入一些其他编程语言的优点,如面向对"
         "象)的LISP。[来源请求]Python的设计哲学强调代码的可读性和简洁的语法("
         "尤其是使用空格缩进划分代码块,而非使用大括号或者关键词)。相比于C++"
         "或Java,Python让开发者能够用更少的代码表达想法。\n不管是小型还是大型"
         "程序,该语言都试图让程序的结构清晰明了。")

text2 = ("Python is an interpreted, high-level, general-purpose programming "
         "language. Created by Guido van Rossum and first released in 1991, "
         "Python's design philosophy emphasizes code readability with its "
         "notable use of significant whitespace.\nIts language constructs and "
         "object-oriented approach aim to help programmers write clear, "
         "logical code for small and large-scale projects.")

font = 'courier 12 bold'
sg.change_look_and_feel('Dark Blue 3')
height = 800

def txt(text):
    return sg.Text(text, font=font, size=(18,1), justification='right')

def cmb(default, key, value):
    return sg.Combo(default_value=default, size=(12,1), values=value,
                enable_events=True, key=key, font=font)

layout = [[
    txt('chars'     ), cmb( 80    ,'chars'     ,(20,40,60,80)),
    txt('pad'       ), cmb( 20    ,'pad'       ,(20,40,60,80)),
    txt('x'         ), cmb(150    ,'x'         ,(0, 150, 300, 800, 1200)),
    txt('y'         ), cmb(700    ,'y'         ,(200,500,700,800))],
   [txt('family'    ), cmb('times','family'    ,('courier','arial','times')),
    txt('font_size' ), cmb(16     ,'font_size' ,(8, 10, 12, 16, 24)),
    txt(' '         ), sg.Button('Move', font=font, size=(13,1))],
   [txt('bold'      ), cmb('False','bold'      ,('False','True')),
    txt('italic'    ), cmb('False','italic'    ,('False','True')),
    txt('underline' ), cmb('False','underline' ,('False','True')),
    txt('overstrike'), cmb('False','overstrike',('False','True'))],
   [txt('color'     ), cmb('black','color'     ,('black','blue','white')),
    txt('bg_color'  ), cmb('white','bg_color'  ,('white','blue','green','No')),
    txt('line_color'), cmb('black','line_color',('black','blue','green'))],
   [sg.Graph(canvas_size=(1600, height),pad=(0,0), key='Graph',
    graph_bottom_left=(0,0), graph_top_right=(1600, height))]]

window = sg.Window('Wrap Text by DrawText', layout=layout, finalize=True)

draw = window['Graph']
T    = Text(draw)

chars       = 80
pad         = 20
x           = 150
y           = 700
color       = 'black'
bg_color    = 'white'
line_color  = 'black'
family      =  'courier'
font_size   = 16
bold        = False
italic      = False
underline   = False
overstrike  = False

index1 = T.DrawText(text1, chars=chars, pad=pad, x=x, y=y,
            color=color, bg_color=bg_color, line_color=line_color,
            family=family, font_size=font_size, bold=bold, italic=italic,
            underline=underline, overstrike=overstrike)
index2 = T.DrawText(text2, chars=chars, pad=pad, x=x, y=y-300,
            color=color, bg_color=bg_color, line_color=line_color,
            family=family, font_size=font_size, bold=bold, italic=italic,
            underline=underline, overstrike=overstrike)

while True:

    event, values = window.read()

    if event    == None         : break

    elif event  == 'chars'      : chars   = values['chars']
    elif event  == 'pad'        : pad     = values['pad']
    elif event  == 'x'          : x   = values['x']
    elif event  == 'y'          : y   = values['y']
    elif event  == 'color'      : color = values['color']
    elif event  == 'bg_color'   : bg_color = values['bg_color']
    elif event  == 'line_color' : line_color = values['line_color']
    elif event  == 'family'     : family = values['family']
    elif event  == 'font_size'  : font_size = values['font_size']
    elif event  == 'bold'       :
        bold = True if values['bold']=='True' else False
    elif event  == 'italic'     :
        italic = True if values['italic']=='True' else False
    elif event  == 'underline'  :
        underline = True if values['underline']=='True' else False
    elif event  == 'overstrike' :
        overstrike = True if values['overstrike']=='True' else False

    if event == 'Move':
        pos = ((20, 0),(20, 0),(20, 0),(20, 0),(10, 0),
               (0,-20),(0,-20),(0,-20),(0,-20),(0,-20),
               (-20,0),(-20,0),(-20,0),(-20,0),(-20,0),
               (0, 20),(0, 20),(0, 20),(0, 20),(0, 20))
        for dx, dy in pos:
            T.move_delta(index1, dx, dy)
            T.move_delta(index2, dx, dy)
            window.refresh()
    else:
        T.Delete(index1)
        T.Delete(index2)
        index1 = T.DrawText(text1, chars=chars, pad=pad, x=x, y=y,
            color=color, bg_color=bg_color, line_color=line_color,
            family=family, font_size=font_size, bold=bold, italic=italic,
            underline=underline, overstrike=overstrike)
        index2 = T.DrawText(text2, chars=chars, pad=pad, x=x, y=y-300,
            color=color, bg_color=bg_color, line_color=line_color,
            family=family, font_size=font_size, bold=bold, italic=italic,
            underline=underline, overstrike=overstrike)

window.close()
PySimpleGUI commented 4 years ago

Very good!

Did you find it necessary to use any tkinter calls this time??

I added several Graph primitives based on the needs of your games. I recently have been working with this little drawing application to help me test the primitives.

https://github.com/PySimpleGUI/PySimpleGUI/blob/master/DemoPrograms/Demo_Graph_Drawing_And_Dragging_Figures.py

You may also want to consider moving over to the lower case PEP8 bindings. draw_text is the better call to use so that it's clear it's a function/method. My earlier call format isn't PEP8 compliant.

I see you're using the Dark Blue 3 theme!

This weekend / today I'm releasing the new "Theme" calls / capabilities. They've already been uploaded to GitHub. theme is the new call to use instead of change_look_and_feel. It will also allow you to find the colors of stuff so you can match if you want.

For example, the background color is found by calling theme_background_color()

I'm trying to make the calls simpler. theme is nice and short and simple.

The new "default theme" is "Dark Blue 3". So if a program doesn't choose a theme, this is the one that will be displayed instead of the normal gray window. I'm hoping no revolt will happen.

PySimpleGUI commented 4 years ago

It's nice to see you using the "user defined element" idea with your cmb and txt . I know they've made many programs much easier for me to understand. Personally, even though they are functions, I would use upper case for Txt and Cmb. It breaks the PEP8 guideline, but I think they act like other Elements and should look like them too. I do this with functions like Submit, Cancel, Ok, etc. Even though they are functions, they're used in layouts and wanted to stay consistent.

I always learn something new, get new ideas, from reading your code.

jason990420 commented 4 years ago

I don't call tkinter directly, but it is always not Simple gui to create a 'element' just for some special case. And, most of jobs done by Python, it's clearly slow. For the theme, just insert change_look_and_feel. statement because warning meesage shown and told me to use it....Hahaha... PEP8...I didn't spend time on it, maybe I will. For 'Drawing and Moving Stuff Around', it's good as a demo script for people. It let me think about how to save the drawing. Maybe using PIL to draw on image directly or tkinter.Canvas.postscript() .

PySimpleGUI commented 4 years ago

I tweeted code to save the drawings once. I'll add it to the demo.

I'm going to get rid of that warning and just do it for people. If they complain then I'll either change it to a different default or tell them to use "Default" and they'll get their plain gray window back.

I don't like the nagging print. It's annoying when you just want to do something quickly. So, I think choosing a good theme like Dark Blue 3 is a good way to go.

Definitely look at PEP8. It should guide how you name things. It's taken a while but I've gotten the PySimpleGUI APIs PEP8 compliant now. The internal names are still messed up, but there's only so much I can do. It's being rewritten anyway.

Thanks for the reply and ideas. You're an inspirational user.