diff --git a/README.md b/README.md index facec00..cb07d56 100644 --- a/README.md +++ b/README.md @@ -16,11 +16,12 @@ Once you have called this, you can create an **infinite loop** and call two func ```py while True: oGUI.startLoop() - + ... oGUI.endLoop() ``` -*Inbetween* the `start` and `end` loop, you can call the drawing functions. +*Inbetween* the `start` and `end` loop, you should execute the update_gui() *ONCE*. +Then it will render all the gui and handle the callbacks for you *Here is an example:* ```py @@ -29,7 +30,7 @@ checkbox = oGUI.Checkbox(oGUI.gray, oGUI.orange, 125, 150, 20, 20) while True: oGUI.startLoop() - checkbox.draw() + oGUI.update_gui() oGUI.endLoop() ``` @@ -54,24 +55,73 @@ These are the available colors: `oGUI.lightgray` `oGUI.darkgray` -**Functions** +**Creating widgets** --------------------- -Creating *checkboxes* +e.g. Creating *checkboxes* To create a **checkbox**, we can create a variable and then call the `oGUI.Checkbox()` function. Usage: ```py -checkbox1 = oGUI.Checkbox(outsideColor, insideColor, x position, y position, width, height, enabledByDefault) +checkbox1 = oGUI.Checkbox(outsideColor, insideColor, x position, y position, width, height, enabledByDefault, callback_function) ``` enabledByDefault is optional, and if you leave it blank (dont specify it), it will be false. We will continue to use *checkbox1* as the *checkbox variable* for the rest of the documentation, and the *rest of these functions* should be called in an **infinite loop.** -To *render* the actual checkbox, we must call its `.draw()` function. Usage: +To *render* the actual checkbox, ~~we must call its `.draw()` function~~ +Now, you will only have to run the update_gui() **ONCE** and it will draw *ALL* the widgets you've created(as long as they're not hidden) for you. +So idealy, you won't have to call the draw function manually. +Usage: ```py -checkbox1.draw() +while True: + oGUI.startLoop() + + oGUI.update_gui() + + oGUI.endLoop() ``` +for more widgets creation, goto [example.py](examples/example.py) + +** We need to put this function inbetween of our `startLoop()` and `endLoop()`. +**Callbacks** +--------------------- +call back is the core of a gui. This allows a funciton to be called once a widget is interacted in a certain way. +e.g. +```py +import oGUI + +oGUI.init() + + +def button_clicked(): + print('I am clicked') + +button = oGUI.Button(oGUI.blue, oGUI.white, 400, 300, 100, 30, text='click me', clicked_callback=button_clicked) + +while True: + oGUI.startLoop() # Start of Draw Loop + + oGUI.update_gui() # handle update and callback + + oGUI.endLoop() # End of Draw Loop +``` +The function *button_clicked* will be executed everytime the button is clicked. +However, the function will be run in the main thread defaultly, +this means the rest of the program and the mainloop would have to wait for the called function to finish. +So if the function takes up a considerable amount of time, the gui will noticibly stop responding. +To fix this, use multithread libs, so that the callback function and the maintheard can run simultaneously. + +Notice that you WON'T want the brackets if you're defining the callback, +Just like in the example, we used button_clicked NOT button_clicked(). +The reason is that if you use *funciton*, the function itself will be passed on to the callback +On the contrary, if you use *function()*, the function itself will be executed and its return will be passed on to the callback. +Normally, we don't want this to happen. + +More examples of callback can be found in the [example.py](examples/example.py) + +**Malipulating widgets** +--------------------- We can also change the *color* of the box if it is hovered over, using the `.is_hovered()` function. Usage: ```py checkbox1.is_hovered(color) diff --git a/examples/example.py b/examples/example.py index 8523eeb..0e9353d 100644 --- a/examples/example.py +++ b/examples/example.py @@ -2,51 +2,67 @@ oGUI.init() -checkbox = oGUI.Checkbox(oGUI.gray, oGUI.orange, 125, 150, 20, 20) -rect = oGUI.Rect(oGUI.darkgray, 100, 100, 300, 500) -box = oGUI.Box(oGUI.lightgray, 100, 100, 300, 500, 5) -button = oGUI.Button(oGUI.gray, oGUI.orange, 120, 200, 30, 30) -button2 = oGUI.Button(oGUI.darkgray, oGUI.lightgray, 368, 103, 30, 35) -myText = oGUI.Text(oGUI.orange, 195, 110, 30, "oGUI Demo") -myText2 = oGUI.Text(oGUI.orange, 155, 152, 25, "Checkbox") -myText3 = oGUI.Text(oGUI.orange, 155, 208, 25, "Button") -myText4 = oGUI.Text(oGUI.black, 375, 105, 30, "X") +def button_clicked(): + print('') + print('button is clicked!') + print('this massage would only appear once after the button is clicked') -while True: - oGUI.startLoop() #Start of Draw Loop +def checkbox_status_changed(): + print('') + print('checkbox status changed') + print('now, checkbox is:', 'checked' if checkbox.is_enabled() else 'unchecked') + print('this massage would only appear once after the checkbox is toggled') - rect.draw() #Drawing Rectangle, Box, Checkbox, and Button - box.draw() - checkbox.draw() - button.draw() - button2.draw() - myText.draw() #Drawing Text - myText2.draw() - myText3.draw() - myText4.draw() +def exit_button_clicked(): + exit(0) - oGUI.endLoop() #End of Draw Loop - checkbox.is_hovered(oGUI.lightgray) #Changes color when checkbox and button(s) is hovered over - button.is_hovered(oGUI.lightgray) - button2.is_hovered(oGUI.gray) +window_x = 100 +window_y = 100 +window_w = 300 +window_h = 500 + +rect = oGUI.Rect(oGUI.darkgray, window_x, window_y, window_w, window_h) +box = oGUI.Box(oGUI.lightgray, window_x, window_y, window_w, window_h, 5) +checkbox = oGUI.Checkbox(oGUI.gray, oGUI.orange, 125, 150, 20, 20, toggled_callback=checkbox_status_changed, + text='checkbox') +button = oGUI.Button(oGUI.gray, oGUI.orange, 120, 200, text='button', clicked_callback=button_clicked) +quit_button = oGUI.Button(oGUI.darkgray, oGUI.lightgray, 368, 103, 30, 35, clicked_callback=exit_button_clicked, + text='×') + +myText = oGUI.Text(oGUI.orange, window_x + window_w / 2, window_y + 5, 30, + "overlayGUI by ethanedits", textAlign=1) + +# DVDCJW - myText.font('Roboto') #Setting Text Object's font - myText2.font('Roboto') - myText3.font('Roboto') +credit_text = oGUI.Text(oGUI.orange, window_x + window_w / 2, window_y + window_h / 2 - 20, 30, + 'major overhaul by DVDCJW', textAlign=1, verticalAlign=1) +credit_text2 = oGUI.Text(oGUI.orange, window_x + window_w / 2, window_y + window_h / 2 + 20, 20, 'including:', + textAlign=1, verticalAlign=1) +credit_text3 = oGUI.Text(oGUI.orange, window_x + window_w / 2, window_y + window_h / 2 + 40, 25, + 'CALLBACK for button and checkbox', textAlign=1, verticalAlign=1) +credit_text4 = oGUI.Text(oGUI.orange, window_x + window_w / 2, window_y + window_h / 2 + 60, 25, 'widgets upgrade', + textAlign=1, verticalAlign=1) +credit_text5 = oGUI.Text(oGUI.orange, window_x + window_w / 2, window_y + window_h / 2 + 80, 20, + 'better checkbox hold logic', textAlign=1, verticalAlign=1) +credit_text6 = oGUI.Text(oGUI.orange, window_x + window_w / 2, window_y + window_h / 2 + 100, 20, + 'integrated text for callable widgets', textAlign=1, verticalAlign=1) - myText.dropShadow(oGUI.black, 2) #Setting Text Object's DropShadow - myText2.dropShadow(oGUI.black, 2) - myText3.dropShadow(oGUI.black, 2) - - if button.is_enabled(): #Do something if the button is enabled/pressed - print('Button was pressed') +# feel free to delete my credits if you're not comfortable with it. +# But I really made this project way more practical, efficient and maintainable - if button2.is_enabled(): #Exit Button - exit(0) +while True: + oGUI.startLoop() # Start of Draw Loop + + oGUI.update_gui() # handle update and callback + + # maybe some of your own pygame code if you'd like - if checkbox.is_enabled(): #Do something if the checkbox is enabled - print('Checkbox is Enabled!') + oGUI.endLoop() # End of Draw Loop + + checkbox.is_hovered(oGUI.lightgray) # Changes color when checkbox and button(s) is hovered over + button.is_hovered(oGUI.lightgray) + quit_button.is_hovered(oGUI.gray) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..81dea35 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pygame +pypiwin32 diff --git a/src/oGUI.py b/src/oGUI.py index e024b0b..0da5e84 100644 --- a/src/oGUI.py +++ b/src/oGUI.py @@ -1,5 +1,19 @@ +import random + import pygame, win32api, win32gui, win32con, time +# A storage dict, stores all buttons, checkboxes. +# Enabled draw_gui function, which draws all elements in the dict. +# And the major update --- Callback function. +# Now, instead of manually checking the status and run a function, +# which would lead to code duplication and stability problems +# You can just assign a function to the element's "callback" attribute, +# and it will be called ONCE when the element is altered. +# +# It stores all the objects + +widgets = [] + version = 0.4 width = win32api.GetSystemMetrics(0) @@ -25,165 +39,288 @@ darkgray = (41, 41, 41) purple = (133, 55, 250) + def init(): - pygame.init() - pygame.display.set_caption('oGUI window') - print('') - print(f'OverlayGUI {version}') - print('oGUI package by EthanEDITS') - print('') - - win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE, - win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE) | win32con.WS_EX_LAYERED) - win32gui.SetLayeredWindowAttributes(hwnd, win32api.RGB(*fuchsia), 0, win32con.LWA_COLORKEY) - win32gui.SetWindowPos(hwnd, win32con.HWND_TOPMOST, 0, 0, 0, 0, win32con.SWP_NOSIZE) - -def startLoop(): - for event in pygame.event.get(): + pygame.init() + pygame.display.set_caption('oGUI window') + print('') + print(f'OverlayGUI {version}') + print('oGUI package by EthanEDITS') + print('') + + +win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE, + win32gui.GetWindowLong(hwnd, + win32con.GWL_EXSTYLE) | win32con.WS_EX_LAYERED | win32con.WS_EX_TOOLWINDOW) +win32gui.SetLayeredWindowAttributes(hwnd, win32api.RGB(*fuchsia), 0, win32con.LWA_COLORKEY) +win32gui.SetWindowPos(hwnd, win32con.HWND_TOPMOST, 0, 0, 0, 0, win32con.SWP_NOSIZE) + + +def startLoop(): + for event in pygame.event.get(): if event.type == pygame.QUIT: exit(0) - screen.fill(fuchsia) + screen.fill(fuchsia) + def endLoop(): - pygame.display.update() - -class Checkbox: - - def __init__(self, outsideColor, insideColor, x, y, width, height, enabledByDefault = False): - self.outsideColor = outsideColor - self.insideColor = insideColor - self.x = x - self.y = y - self.width = width - self.height = height - self.checkBox_enabled = enabledByDefault - self.is_hoverable = False - self.hover_color = gray - self.boolMousePos = False - - def is_hovered(self, hoveredColor): - self.is_hoverable = True - self.hover_color = hoveredColor - - def printMousePos(self): - self.boolMousePos = True - - def is_enabled(self): - return self.checkBox_enabled - - def draw(self): - pygame.draw.rect(screen, self.outsideColor, pygame.Rect(self.x - self.width/8, self.y - self.height/8, self.width + self.width/4, self.height + self.height/4)) - - mouse = pygame.mouse - - if self.x + self.width > mouse.get_pos()[0] > self.x and self.y + self.height > mouse.get_pos()[1] > self.y: - #When Hovered Over - if self.is_hoverable: - pygame.draw.rect(screen, self.hover_color, pygame.Rect(self.x - self.width/8, self.y - self.height/8, self.width + self.width/4, self.height + self.height/4)) - - #When clicked - if mouse.get_pressed()[0]: - self.checkBox_enabled = not self.checkBox_enabled - time.sleep(0.15) - - if self.checkBox_enabled: - pygame.draw.rect(screen, self.insideColor, pygame.Rect(self.x, self.y, self.width, self.height)) - - if self.boolMousePos: - print(mouse.get_pos()) - -class Rect: - - def __init__(self, color, x, y, width, height): - self.color = color - self.x = x - self.y = y - self.width = width - self.height = height - - def draw(self): - pygame.draw.rect(screen, self.color, pygame.Rect(self.x, self.y, self.width, self.height)) - -class Box: - - def __init__(self, color, x, y, width, height, thickness): - self.color = color - self.x = x - self.y = y - self.width = width - self.height = height - self.thickness = thickness - - def draw(self): - pygame.draw.line(screen, self.color, (self.x + self.width, self.y), (self.x, self.y), self.thickness) #Top - pygame.draw.line(screen, self.color, (self.x, self.y + self.height), (self.x, self.y), self.thickness) #Left - pygame.draw.line(screen, self.color, (self.x + self.width, self.y), (self.x + self.width, self.y + self.height), self.thickness) #Right - pygame.draw.line(screen, self.color, (self.x, self.y + self.height), (self.x + self.width, self.y + self.height), self.thickness) #Bottom - -class Text: - - def __init__(self, color, x, y, FontSize, textStr): - pygame.font.init() - self.color = color - self.x = x - self.y = y - self.FontSize = FontSize - self.textStr = textStr - self.FontString = 'Arial' - #DropShadow - self.dropShadowEnabled = False - self.dropShadowColor = black - self.dropShadowOffset = 2 - - def font(self, fontStr): - self.FontString = fontStr - - def dropShadow(self, color, offset): - self.dropShadowEnabled = True - self.dropShadowColor = color - self.dropShadowOffset = offset - - def draw(self): - myfont = pygame.font.SysFont(self.FontString, self.FontSize) - textSurface = myfont.render(self.textStr, True, self.color) #Main Text - - if self.dropShadowEnabled: - textSurface2 = myfont.render(self.textStr, True, black) #DropShadow - screen.blit(textSurface2, (self.x + self.dropShadowOffset, self.y)) #DropShadow - - screen.blit(textSurface, (self.x, self.y)) #Main Text - -class Button: - def __init__(self, color, clickedColor, x,y,width,height, text=''): - self.color = color - self.clickedColor = clickedColor - self.x = x - self.y = y - self.width = width - self.height = height - self.text = text - self.is_hoverable = False - self.hover_color = gray - self.button_enabled = False + pygame.display.update() + + +class WidgetBasics: + def __init__(self, is_hidden=False): + global widgets + widgets.append(self) + + def hide(self): + self.is_hidden = True + + def show(self): + self.is_hidden = False + + +class Rect(WidgetBasics): + def __init__(self, color, x, y, width, height): + super().__init__() + self.callable = False + self.color = color + self.x = x + self.y = y + self.width = width + self.height = height + + def draw(self): + pygame.draw.rect(screen, self.color, pygame.Rect(self.x, self.y, self.width, self.height)) + + +class Box(WidgetBasics): + + def __init__(self, color, x, y, width, height, thickness): + super().__init__() + self.callable = False + self.color = color + self.x = x + self.y = y + self.width = width + self.height = height + self.thickness = thickness + + def draw(self): + pygame.draw.line(screen, self.color, (self.x + self.width, self.y), (self.x, self.y), self.thickness) # Top + pygame.draw.line(screen, self.color, (self.x, self.y + self.height), (self.x, self.y), self.thickness) # Left + pygame.draw.line(screen, self.color, (self.x + self.width, self.y), (self.x + self.width, self.y + self.height), + self.thickness) # Right + pygame.draw.line(screen, self.color, (self.x, self.y + self.height), + (self.x + self.width, self.y + self.height), self.thickness) # Bottom + + +class Text(WidgetBasics): + + def __init__(self, color, x, y, FontSize, textStr, dropShadowEnabled=True, textAlign=0, verticalAlign=0): + super().__init__() + pygame.font.init() + self.callable = False + self.color = color + self.x = x + self.y = y + self.FontSize = FontSize + self.textStr = textStr + self.FontString = 'Roboto' + self.dropShadowColor = black + self.dropShadowOffset = 2 + self.dropShadowEnabled = dropShadowEnabled + self.textAlign = textAlign + self.verticalAlign = verticalAlign + + def font(self, fontStr): + self.FontString = fontStr + + def dropShadow(self, color, offset): + self.dropShadowEnabled = True + self.dropShadowColor = color + self.dropShadowOffset = offset + + def setTextAlign(self, textAlign): + self.textAlign = textAlign + + def setVerticalAlign(self, verticalAlign): + self.verticalAlign = verticalAlign + + def draw(self): + myfont = pygame.font.SysFont(self.FontString, self.FontSize) + textSurface = myfont.render(self.textStr, True, self.color) # Main Text + text_w, text_h = myfont.size(self.textStr) + textRect = textSurface.get_rect() + + if self.textAlign == 0: + x = self.x # Left Align + elif self.textAlign == 1: + x = self.x - text_w // 2 # Center + elif self.textAlign == 2: + x = self.x - text_w # Right Align + + if self.verticalAlign == 0: + y = self.y # Top Align + elif self.verticalAlign == 1: + y = self.y - text_h // 2 # Center + elif self.verticalAlign == 2: + y = self.y - text_h # Bottom Align + + textRect = (x, y) + + if self.dropShadowEnabled: + textSurface2 = myfont.render(self.textStr, True, black) # DropShadow + textRect2 = (textRect[0] + self.dropShadowOffset, textRect[1]) + screen.blit(textSurface2, textRect2) # DropShadow + + screen.blit(textSurface, textRect) # Text + + +class Button(WidgetBasics): + # since the term 'press' emphasizes the hold action, and click is more appropriate for a single action, + # the handle-anti-multi-trigger-per-click variables is names after 'press', while the callback is names after click + def __init__(self, color, clickedColor, x, y, width=None, height=30, text='', clicked_callback=None): + super().__init__() + self.type = 'button' + self.clicked_callback = clicked_callback + self.callable = True + self.last_frame_pressed = False + self.color = color + self.clickedColor = clickedColor + self.x = x + self.y = y + self.width = width + self.height = height + + w, h = pygame.font.SysFont('Roboto', int(self.height)).size(text) + if not self.width: + self.width = w + 10 + self.text_widget = Text(self.clickedColor, self.x + self.width // 2, self.y + self.height // 2, self.height, + text, verticalAlign=1, textAlign=1) + + self.is_hoverable = True + self.hover_color = (self.clickedColor[0] / 2, self.clickedColor[1] / 2, self.clickedColor[2] / 2) + self.pressed = False def is_enabled(self): - return self.button_enabled + return self.pressed def is_hovered(self, hoveredColor): - self.is_hoverable = True - self.hover_color = hoveredColor + self.is_hoverable = True + self.hover_color = hoveredColor + + def draw(self): + pygame.draw.rect(screen, self.color, pygame.Rect(self.x, self.y, self.width, self.height)) + + mouse = pygame.mouse + + if self.x + self.width > mouse.get_pos()[0] > self.x and self.y + self.height > mouse.get_pos()[1] > self.y: + if self.is_hovered: + pygame.draw.rect(screen, self.hover_color, pygame.Rect(self.x, self.y, self.width, self.height)) + + if mouse.get_pressed()[0]: + self.pressed = True + pygame.draw.rect(screen, self.clickedColor, pygame.Rect(self.x, self.y, self.width, self.height)) + else: + self.pressed = False + + +class Checkbox(WidgetBasics): + def __init__(self, outsideColor, insideColor, x, y, width, height, text=None, checked_callback=None, + toggled_callback=None, checkedByDefault=False): + super().__init__() + self.callable = True + self.checked_callback = checked_callback + self.toggled_callback = toggled_callback + self.type = 'checkbox' + self.last_frame_checked = checkedByDefault + self.outsideColor = outsideColor + self.insideColor = insideColor + self.x = x + self.y = y + self.mouse_holding = False + self.width = width + self.height = height + self.checked = checkedByDefault + self.is_hoverable = True + self.hover_color = (self.insideColor[0] / 2, self.insideColor[1] / 2, self.insideColor[2] / 2) + self.boolMousePos = False + + w, h = pygame.font.SysFont('Roboto', int(self.height)).size(text) + self.text_widget = Text(self.insideColor, self.x + 10 + self.width, self.y + self.height // 2, self.height, + text, verticalAlign=1, textAlign=0) + + def is_hovered(self, hoveredColor): + self.is_hoverable = True + self.hover_color = hoveredColor + + def printMousePos(self): + self.boolMousePos = True + + def is_enabled(self): + return self.checked def draw(self): - pygame.draw.rect(screen, self.color, pygame.Rect(self.x, self.y, self.width, self.height)) - - mouse = pygame.mouse - - if self.x + self.width > mouse.get_pos()[0] > self.x and self.y + self.height > mouse.get_pos()[1] > self.y: - if self.is_hoverable: - pygame.draw.rect(screen, self.hover_color, pygame.Rect(self.x, self.y, self.width, self.height)) - - if mouse.get_pressed()[0]: - self.button_enabled = True - pygame.draw.rect(screen, self.clickedColor, pygame.Rect(self.x, self.y, self.width, self.height)) - else: - self.button_enabled = False + pygame.draw.rect(screen, self.outsideColor, + pygame.Rect(self.x - self.width / 8, self.y - self.height / 8, self.width + self.width / 4, + self.height + self.height / 4)) + + mouse = pygame.mouse + + if self.x + self.width > mouse.get_pos()[0] > self.x and self.y + self.height > mouse.get_pos()[1] > self.y: + # When Hovered Over + if self.is_hoverable: + pygame.draw.rect(screen, self.hover_color, + pygame.Rect(self.x - self.width / 8, self.y - self.height / 8, + self.width + self.width / 4, self.height + self.height / 4)) + + # When clicked + check if mouse was continuously held in the previous frames + if mouse.get_pressed()[0]: + if not self.mouse_holding: + self.checked = not self.checked + self.mouse_holding = True + else: + self.mouse_holding = False + + if self.checked: + pygame.draw.rect(screen, self.insideColor, pygame.Rect(self.x, self.y, self.width, self.height)) + + if self.boolMousePos: + print(mouse.get_pos()) + + +def update_gui(): + # the startLoop and endLoop would not be included in this function. + # In case the use creates something else not via this library but using pygame itself, + # they can use the startLoop and endLoop to wrap around their own code + for i in widgets: + i.draw() + handle_gui_alter() + + +def handle_checkbox_callback(checkbox_object): + if checkbox_object.last_frame_checked != checkbox_object.checked: + if checkbox_object.toggled_callback: + checkbox_object.toggled_callback() # this line executes the callback function while the last detects if there is one + if checkbox_object.checked: + if checkbox_object.checked_callback: + checkbox_object.checked_callback() + + checkbox_object.last_frame_checked = checkbox_object.checked + + +def handle_button_callback(button_object): + if button_object.last_frame_pressed != button_object.pressed: + if button_object.pressed: + if button_object.clicked_callback: + button_object.clicked_callback() + button_object.last_frame_pressed = button_object.pressed + + +def handle_gui_alter(): + for i in widgets: + if i.callable: + if i.type == 'checkbox': + handle_checkbox_callback(i) + if i.type == 'button': + handle_button_callback(i)