Related
How can I split this class into several simpler ones, using the principle of single responsibility?
There is this awful class:
# engine for game logic
import HB_classLib as cllib
from card_mech import *
from pygame import Color, display
import itertools as itools
from turn import Turn
def genSlots(details, screen):
"""Generating slots for cards"""
base = [10, 570, 85, 670]
for i in range(8):
typlpx = 420 + (i - 4) * 95 if i >= 4 else base[0] + i * 95
l1 = [typlpx, base[1], typlpx + 75, base[1]]
l2 = [typlpx, base[1], typlpx, base[3]]
l3 = [typlpx, base[3], typlpx + 75, base[3]]
l4 = [typlpx + 75, base[1], typlpx + 75, base[3]]
details.append(cllib.CardSlot(screen, [l1, l2, l3, l4],
Color("white"), bn=3))
class Engine:
"""Engine which will calculate gameClasses's interactions"""
def __init__(self, screen, usnames, mode=1):
self.names_frame = (cllib.Text(usnames[0].get_value(), 36, Color("white")),
cllib.Text(usnames[1].get_value(), 36, Color("white")))
self.screen = screen
self.mode = mode
self.mod_buttons = [cllib.Button((260, 340), (300, 90), Color("gold"),
"Divided field", None, 54, xIndF=40, yIndF=30),
cllib.Button((260, 450), (300, 90), Color("yellow"),
"No man's land", None, 54, xIndF=20, yIndF=30)]
self.start_cards = [MeleeCard(Image_fn="images/card1.jpg", pos=(10, 575), health=5, attack=3),
CavalryCard(Image_fn="images/card2.xcf", pos=(108, 575), health=3, attack=3),
CavalryCard(Image_fn="images/card3.xcf", pos=(198, 575), health=5, attack=4)]
self.pl = Player()
self.pl2 = Player()
self.map = cllib.Map()
self.currlvlmap = cllib.LvlMap()
self.borderline = cllib.Line(self.screen, [400, 0, 400, 480], "black", 3)
self.blno_mans_land = [cllib.Line(self.screen, [240, 0, 240, 480], "blue", 3),
cllib.Line(self.screen, [560, 0, 560, 480], "red", 3)]
self.turnButton = cllib.Button((360, 485), (80, 80), Color("#E0FFFF"),
"End Turn", None, 24,
xIndF=5, yIndF=35, shape='round', fontColor="#800000")
self.turnArrow = cllib.Image("images/turn_arrow.jpg", (360, 680), imSize=(90, 120))
self.turnFight = cllib.Image("images/fight.jpg", (360, 680), imSize=(90, 120))
self.turneng = Turn(self.pl, self.pl2, self)
self.details = [self.currlvlmap, self.turnButton, self.turnArrow, self.start_cards]
self.cardpos_acc = [i.pos for i in self.start_cards]
def up_phase(self):
"""Increase phase ratio unconditionly"""
+self.turneng
def check_turns(self, transformed, is_increase=True):
self.up_phase() if is_increase else None
self.turneng.do_logic()
match self.turneng.phase:
case 0:
self.details[2] = cllib.Image(self.turnArrow.image,
(360, 680), imSize=(90, 120)
)
case 1:
self.details[2] = cllib.Image(transformed,
(360, 680), imSize=(90, 120))
case 2:
self.details[2] = cllib.Image(self.turnFight.image,
(360, 680), imSize=(90, 120)
)
def start(self):
"""Create default settings and init needed classes, draw map with mods buttons"""
self.pl.hand.fill([self.start_cards[0], self.start_cards[1]])
self.pl2.hand.fill([self.start_cards[2]])
self.map.fill_with(self.mod_buttons)
self.map.draw(self.screen)
def turn_level(self):
"""Turn on level, generate and draw level's map, setting up selected mode"""
self.map.should_draw = False
for btn in self.mod_buttons:
btn.keepOn = False
self.currlvlmap.set(self.screen)
genSlots(self.details, self.screen)
def update_map(self):
self.map.draw(self.screen)
def update_level(self):
nested = lambda b: b is self.start_cards or isinstance(b, list | tuple)
for det in self.details:
det.draw(self.screen) if not nested(det) else [atom.draw(self.screen) for atom in det]
blitter = lambda align, factor: (
self.screen.blit(name.image, (align + index * factor - len(str(name.text)) * 6, 500))
for index, name in enumerate(self.names_frame)
)
match self.mode:
case 1:
self.borderline.draw()
blitter(200, 400)
case 2:
for el in self.blno_mans_land: el.draw()
blitter(120, 560)
display.flip()
def fight(self):
pass
I tried to separate this class into a Starter class, an Updater class, a TurnChecker class, and a FightMechanicsExecutor class, but the result was that each class was dependent on each other and, of course, this structure was not viable.
In addition, I have already moved the function genSlots outside the class, but this doesn't greatly simplify the code.
Basically I am trying to create a radial menu like this
I was using QPainter and here is a attempt from my side. But I can't figure out how to add a click event on the pixmaps. Is there any lib available for this ?
Images Link
from PySide2 import QtWidgets, QtGui, QtCore
import sys
import os
RESPATH = "/{your_folder}/radialMenu/res"
class RadialMenu(QtWidgets.QGraphicsRectItem):
addButton = 1
disableButton = 2
clearButton = 3
exportButton = 4
infoButton = 5
runButton = 6
scriptsButton = 7
def __init__(self, parent=None):
super(RadialMenu, self).__init__(parent)
def paint(self, painter, option, widget=None):
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.setBrush(QtGui.QBrush(QtGui.QColor(71, 71, 71, 0)))
tempPen = QtGui.QPen(QtGui.QColor(178, 141, 58), 20.0, QtCore.Qt.CustomDashLine)
tempPen.setDashPattern([4, 4, 4, 4])
painter.setPen(tempPen)
painter.drawEllipse(0, 0, 150, 150)
topX = 0
topY = 0
pixmap1 = QtGui.QPixmap(os.path.join(RESPATH, "add.png"))
painter.drawPixmap(topX + 50, topY - 8, pixmap1)
pixmap2 = QtGui.QPixmap(os.path.join(RESPATH, "disable.png"))
painter.drawPixmap(topX + 90, topY - 5, pixmap2)
pixmap3 = QtGui.QPixmap(os.path.join(RESPATH, "clear.png"))
painter.drawPixmap(topX - 10, topY + 70, pixmap3)
pixmap4 = QtGui.QPixmap(os.path.join(RESPATH, "export.png"))
pixmap4 = pixmap4.transformed(QtGui.QTransform().rotate(15))
painter.drawPixmap(topX - 2, topY + 100, pixmap4)
pixmap5 = QtGui.QPixmap(os.path.join(RESPATH, "info.png"))
painter.drawPixmap(topX + 20, topY + 125, pixmap5)
pixmap6 = QtGui.QPixmap(os.path.join(RESPATH, "run.png"))
painter.drawPixmap(topX + 113, topY + 125, pixmap6)
pixmap6 = QtGui.QPixmap(os.path.join(RESPATH, "scripts.png"))
painter.drawPixmap(topX + 137, topY + 85, pixmap6)
class RadialTest(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
self.scene=QtWidgets.QGraphicsScene(self)
buttonItem = RadialMenu()
self.scene.addItem(buttonItem)
buttonItem.setPos(100,100)
buttonItem.setZValue(1000)
self.scene.update()
self.view = QtWidgets.QGraphicsView(self.scene, self)
self.scene.setSceneRect(0, 0, 300, 300)
self.setGeometry(50, 50, 305, 305)
self.show()
if __name__ == "__main__":
app=QtWidgets.QApplication(sys.argv)
firstScene = RadialTest()
sys.exit(app.exec_())
This code will give this result
For this kind of objects, keeping a hierarchical object structure is always suggested. Also, when dealing with object that possibly have "fixed" sizes (like images, but not only), fixed positioning can be tricky expecially with newer system that support different DPI screen values.
With this approach I'm not using mapped images at all (but button icons can be still set), instead I chose to use a purely geometrical concept, using pixel "radius" values and angles for each button.
from PyQt5 import QtWidgets, QtGui, QtCore
from math import sqrt
class RadialMenu(QtWidgets.QGraphicsObject):
buttonClicked = QtCore.pyqtSignal(int)
def __init__(self, parent=None):
super().__init__(parent)
self.setAcceptHoverEvents(True)
self.buttons = {}
def addButton(self, id, innerRadius, size, startAngle, angleSize, pen=None,
brush=None, icon=None):
# if a button already exists with the same id, remove it
if id in self.buttons:
oldItem = self.buttons.pop(id)
if self.scene():
self.scene().removeItem(oldItem)
oldItem.setParent(None)
# compute the extents of the inner and outer "circles"
startRect = QtCore.QRectF(
-innerRadius, -innerRadius, innerRadius * 2, innerRadius * 2)
outerRadius = innerRadius + size
endRect = QtCore.QRectF(
-outerRadius, -outerRadius, outerRadius * 2, outerRadius * 2)
# create the circle section path
path = QtGui.QPainterPath()
# move to the start angle, using the outer circle
path.moveTo(QtCore.QLineF.fromPolar(outerRadius, startAngle).p2())
# draw the arc to the end of the angle size
path.arcTo(endRect, startAngle, angleSize)
# draw a line that connects to the inner circle
path.lineTo(QtCore.QLineF.fromPolar(innerRadius, startAngle + angleSize).p2())
# draw the inner circle arc back to the start angle
path.arcTo(startRect, startAngle + angleSize, -angleSize)
# close the path back to the starting position; theoretically unnecessary,
# but better safe than sorry
path.closeSubpath()
# create a child item for the "arc"
item = QtWidgets.QGraphicsPathItem(path, self)
item.setPen(pen if pen else (QtGui.QPen(QtCore.Qt.transparent)))
item.setBrush(brush if brush else QtGui.QColor(180, 140, 70))
self.buttons[id] = item
if icon is not None:
# the maximum available size is at 45 degrees, use the Pythagorean
# theorem to compute it and create a new pixmap based on the icon
iconSize = int(sqrt(size ** 2 / 2))
pixmap = icon.pixmap(iconSize)
# create the child icon (pixmap) item
iconItem = QtWidgets.QGraphicsPixmapItem(pixmap, self)
# push it above the "arc" item
iconItem.setZValue(item.zValue() + 1)
# find the mid of the angle and put the icon there
midAngle = startAngle + angleSize / 2
iconPos = QtCore.QLineF.fromPolar(innerRadius + size * .5, midAngle).p2()
iconItem.setPos(iconPos)
# use the center of the pixmap as the offset for centering
iconItem.setOffset(-pixmap.rect().center())
def itemAtPos(self, pos):
for button in self.buttons.values():
if button.shape().contains(pos):
return button
def checkHover(self, pos):
hoverButton = self.itemAtPos(pos)
for button in self.buttons.values():
# set a visible border only for the hovered item
button.setPen(QtCore.Qt.red if button == hoverButton else QtCore.Qt.transparent)
def hoverEnterEvent(self, event):
self.checkHover(event.pos())
def hoverMoveEvent(self, event):
self.checkHover(event.pos())
def hoverLeaveEvent(self, event):
for button in self.buttons.values():
button.setPen(QtCore.Qt.transparent)
def mousePressEvent(self, event):
clickButton = self.itemAtPos(event.pos())
if clickButton:
for id, btn in self.buttons.items():
if btn == clickButton:
self.buttonClicked.emit(id)
def boundingRect(self):
return self.childrenBoundingRect()
def paint(self, qp, option, widget):
# required for QGraphicsObject subclasses
pass
ButtonData = [
(50, 40, QtWidgets.QStyle.SP_MessageBoxInformation),
(90, 40, QtWidgets.QStyle.SP_MessageBoxQuestion),
(180, 20, QtWidgets.QStyle.SP_FileDialogBack),
(200, 20, QtWidgets.QStyle.SP_DialogOkButton),
(220, 20, QtWidgets.QStyle.SP_DialogOpenButton),
(290, 30, QtWidgets.QStyle.SP_ArrowDown),
(320, 30, QtWidgets.QStyle.SP_ArrowUp),
]
class RadialTest(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
self.scene = QtWidgets.QGraphicsScene(self)
buttonItem = RadialMenu()
self.scene.addItem(buttonItem)
buttonItem.buttonClicked.connect(self.buttonClicked)
for index, (startAngle, extent, icon) in enumerate(ButtonData):
icon = self.style().standardIcon(icon, None, self)
buttonItem.addButton(index, 64, 20, startAngle, extent, icon=icon)
buttonItem.setPos(150, 150)
buttonItem.setZValue(1000)
self.view = QtWidgets.QGraphicsView(self.scene, self)
self.view.setRenderHints(QtGui.QPainter.Antialiasing)
self.scene.setSceneRect(0, 0, 300, 300)
self.setGeometry(50, 50, 305, 305)
self.show()
def buttonClicked(self, id):
print('Button id {} has been clicked'.format(id))
And this is the result:
Currently this is what I'm doing:
class NewShip:
def __init__(self, position = (0,0), x_velocity = 25, y_velocity = 15, anim_frame = 1, frame1 = "space_ship_1.png", frame2 = "space_ship_2.png", angle = 0, border_size = 900):
self.position = position
self.angle = angle
self.x_velocity = x_velocity #in pixels per frame
self.y_velocity = y_velocity #in pixels per frame
self.anim_frame = anim_frame
self.frame1 = pygame.image.load(frame1).convert_alpha()
self.frame2 = pygame.image.load(frame2).convert_alpha()
self.frame1 = pygame.transform.scale(self.frame1, (128,128))
self.frame2 = pygame.transform.scale(self.frame2, (128,128))
self.position = DummyObject()
self.position.x = position[0]
self.position.y = position[1]
self.border_size = border_size
but honestly that's quite tedious and makes adding any more facultative attributes a real pain.
Should I just skip the whole 'letting people declare attributes with keyword arguments' thing and just instantiate objects in a fully default state before changing any of their attributes?
EDIT:
apparently this works perfectly:
class NewShip:
def __init__(self, **kwargs):
self.angle = 0
self.x_velocity = 25 #in pixels per frame
self.y_velocity = 15 #in pixels per frame
self.anim_frame = 1
frame1 = "space_ship_1.png"
frame2 = "space_ship_2.png"
self.position = DummyObject()
self.position.x = 0
self.position.y = 0
self.border_size = 900
for element in kwargs: setattr(self, element, kwargs[element])
self.frame1 = pygame.image.load(frame1).convert_alpha()
self.frame2 = pygame.image.load(frame2).convert_alpha()
self.frame1 = pygame.transform.scale(self.frame1, (128,128))
self.frame2 = pygame.transform.scale(self.frame2, (128,128))
that is, just setting all defaults values then overriding them with the keyword args using for element in kwargs: setattr(self, element, kwargs[element])
Let me know if there's any cleaner way, though I have to say I'm much more satisfied with this.
To reduce the clutter, you could alternatively define a defaults dictionary attribute _init_attrs for example, define your function with **kwargs and then setattr on self with a loop, using _init_attrs[k] as a default value for kwargs.get when no parameters were supplied:
class NewShip:
def __init__(self, **kwargs):
_init_attrs = {'angle': 0,
'anim_frame': 1,
'border_size': 900,
'frame1': 'space_ship_1.png',
'frame2': 'space_ship_2.png',
'position': (0, 0),
'x_velocity': 25,
'y_velocity': 15 }
for k, v in _init_attrs.items():
setattr(self, k, kwargs.get(k, v)
This makes __init__ a bit more mystifying but achieves the goal of making it way more extensible while cutting down the size.
I am running a creature simulator in python 2.7 using tkinter as my visualizer. The map is made up of squares, where colors represent land types, and a red square represents the creature. I use canvas.move to move that red square around the board. It has to move quite a lot. But I know exactly where it should start and where it should end. The problem is, instead of moving, most of the time it just disappears. In the code below I call move in Simulation's init, and it works. When I call it any time in sim.simulate, the creature just disappears. Can anyone explain why?
class Map():
def __init__(self,):
self.root = Tk()
self.canvas = Canvas(self.root, width=1200, height=1200)
self.canvas.pack()
self.colors = {
"Land": "grey",
"Food": "green",
"Water": "blue",
"Shelter": "black"
}
self.canvasDict = {} # the keys are (x,y, "type"), the data is the id so it can be grabbed for item config.
for i, row in enumerate(land.landMass):
for j, tile in enumerate(row):
color = self.colors[tile.__class__.__name__]
self.canvasDict[i, j, "tile"] = self.canvas.create_rectangle(50 * i, 50 * j, 50 * (i + 1), 50 * (j + 1),
outline=color, fill=color)
info = tile.elevation
if color == "green":
info = tile.vegitation
elif color == "black":
info = tile.quality
self.canvasDict[i, j, "text"] = self.canvas.create_text(50 * i + 3, 50 * j, anchor=NW, fill="white", text=info)
self.canvasDict["creature"] = self.canvas.create_rectangle(0, 0, 50, 50,
outline="red", fill="red")
self.canvas.pack(fill=BOTH, expand=1)
sim = Simulation([], 1, 2, self.root, self.canvas, self.canvasDict)
self.root.after(1000, sim.simulate)
...
other functions
...
def simulate(self):
self.canvas.move(self.canvasDict["creature"], 1, 1)
if self.generations > 0:
self.root.after(10000, self.canvas.move, self.canvasDict["creature"], 2 * 50, 2 * 50)
...
I finally realized what was happening. I made the mistake of thinking that .move would move the object to that location on the canvas, instead it is moving it by that much. So when my square 'disappears' it is really just moving of the visible canvas. I thought that the .after would stall the movements so that I could see that happen, but apparently not.
Pretty much exactly as it sounds. I have buttons in a Wx.Frame that are created on the fly and I'd like the parent frame to increase in height as I add new buttons. The height is already being acquire from the total number of buttons multiplied by an integer equal the each button's height, but I don't know how to get the frame to change size based on that when new buttons are added.
As a side question the current method I have for updating the buttons creates a nasty flicker and I was wondering if anyone had any ideas for fixing that.
import wx
import mmap
import re
class pt:
with open('note.txt', "r+") as note:
buf = mmap.mmap(note.fileno(), 0)
TL = 0
readline = buf.readline
while readline():
TL += 1
readlist = note.readlines()
note.closed
class MainWindow(wx.Frame):
def __init__(self, parent, title):
w, h = wx.GetDisplaySize()
self.x = w * 0
self.y = h - bdepth
self.container = wx.Frame.__init__(self, parent, title = title, pos = (self.x, self.y), size = (224, bdepth), style = wx.STAY_ON_TOP)
self.__DoButtons()
self.Show(True)
def __DoButtons(self):
for i, line in enumerate(pt.readlist):
strip = line.rstrip('\n')
todo = strip.lstrip('!')
self.check = re.match('!', strip)
self.priority = re.search('(\!$)', strip)
if self.check is None and self.priority is None:
bullet = wx.Image('bullet.bmp', wx.BITMAP_TYPE_BMP)
solid = wx.EmptyBitmap(200,64,-1)
dc = wx.MemoryDC()
dc.SelectObject(solid)
solidpen = wx.Pen(wx.Colour(75,75,75),wx.SOLID)
dc.SetPen(solidpen)
dc.DrawRectangle(0, 0, 200, 64)
dc.SetTextForeground(wx.Colour(255, 255, 255))
dc.DrawBitmap(wx.BitmapFromImage(bullet, 32), 10, 28)
dc.DrawText(todo, 30, 24)
dc.SelectObject(wx.NullBitmap)
hover = wx.EmptyBitmap(200,64,-1)
dc = wx.MemoryDC()
dc.SelectObject(hover)
hoverpen = wx.Pen(wx.Colour(100,100,100),wx.SOLID)
dc.SetPen(hoverpen)
dc.DrawRectangle(0, 0, 200, 64)
dc.SetTextForeground(wx.Colour(255, 255, 255))
dc.DrawBitmap(wx.BitmapFromImage(bullet, 32), 10, 28)
dc.DrawText(todo, 30, 24)
dc.SelectObject(wx.NullBitmap)
bmp = solid
elif self.priority is None:
checkmark = wx.Image('check.bmp', wx.BITMAP_TYPE_BMP)
checked = wx.EmptyBitmap(200,64,-1)
dc = wx.MemoryDC()
dc.SelectObject(checked)
checkedpen = wx.Pen(wx.Colour(50,50,50),wx.SOLID)
dc.SetPen(checkedpen)
dc.DrawRectangle(0, 0, 200, 50)
dc.SetTextForeground(wx.Colour(200, 255, 0))
dc.DrawBitmap(wx.BitmapFromImage(checkmark, 32), 6, 24)
dc.DrawText(todo, 30, 24)
dc.SelectObject(wx.NullBitmap)
bmp = checked
else:
exclaim = wx.Image('exclaim.bmp', wx.BITMAP_TYPE_BMP)
important = wx.EmptyBitmap(200,64,-1)
dc = wx.MemoryDC()
dc.SelectObject(important)
importantpen = wx.Pen(wx.Colour(75,75,75),wx.SOLID)
dc.SetPen(importantpen)
dc.DrawRectangle(0, 0, 200, 50)
dc.SetTextForeground(wx.Colour(255, 180, 0))
dc.DrawBitmap(wx.BitmapFromImage(exclaim, 32), 6, 24)
dc.DrawText(todo, 30, 24)
dc.SelectObject(wx.NullBitmap)
importanthover = wx.EmptyBitmap(200,64,-1)
dc = wx.MemoryDC()
dc.SelectObject(importanthover)
importanthoverpen = wx.Pen(wx.Colour(100,100,100),wx.SOLID)
dc.SetPen(importanthoverpen)
dc.DrawRectangle(0, 0, 200, 50)
dc.SetTextForeground(wx.Colour(255, 180, 0))
dc.DrawBitmap(wx.BitmapFromImage(exclaim, 32), 6, 24)
dc.DrawText(todo, 30, 24)
dc.SelectObject(wx.NullBitmap)
bmp = important
b = wx.BitmapButton(self, i + 800, bmp, (10, i * 64), (bmp.GetWidth(), bmp.GetHeight()), style = wx.NO_BORDER)
if self.check is None and self.priority is None:
b.SetBitmapHover(hover)
elif self.priority is None:
b.SetBitmapHover(checked)
else:
b.SetBitmapHover(importanthover)
self.input = wx.TextCtrl(self, -1, "", (16, pt.TL * 64 + 4), (184, 24))
self.Bind(wx.EVT_TEXT_ENTER, self.OnEnter, self.input)
def OnClick(self, event):
button = event.GetEventObject()
button.None
print('cheese')
def OnEnter(self, event):
value = self.input.GetValue()
pt.readlist.append('\n' + value)
self.__DoButtons()
with open('note.txt', "r+") as note:
for item in pt.readlist:
note.write("%s" % item)
note.closed
bdepth = pt.TL * 64 + 32
app = wx.App(False)
frame = MainWindow(None, "Sample editor")
app.SetTopWindow(frame)
app.MainLoop()
AFAIK there no way automatically resize the frame, but you can manually reset the size of your frame with SetSize()
e.g.
w, h = self.GetClientSize()
self.SetSize((w, h + height_of_your_new_button))
To the get the desired result though with minimum hassle you'll need to use sizers, I don't think theres ever a good reason to use absolute positioning. I would also recommend using a panel, which provides tab traversal between widgets and cross platform consistency of layout.
Zetcode Sizer Tutorial
wxPython Sizer Tutorial
Don't double-prefix your methods unless you know what you're doing. This is not directly related to your question, but it'll result in bugs you won't understand later.
See this stackoverflow question and the python documentation what/why.