Python 创建多个Kivy画布图像并导出到png:如何在管理Kivy.clock的不可预测性的同时做到这一点?

Python 创建多个Kivy画布图像并导出到png:如何在管理Kivy.clock的不可预测性的同时做到这一点?,python,python-3.x,kivy,Python,Python 3.x,Kivy,下面我创建了一个简化的Python代码示例。基本上,程序在图像上生成十字,其位置由一些数据文件指定。然后将图像和画布保存到png文件中。它对数据文件中的不同行重复执行多次(下面我只是使用了一个“pos_列表”进行演示) 我的问题是,为了使用export_to_png,必须允许屏幕更新并显示画布指令中的更改。这意味着我需要使用Kivy时钟。然而。当我安排多个时钟事件时,我无法保证它们会出于某种原因以正确的顺序发生(即使是精心选择的计时)。这是一个问题,因为如果PNG是在画布指令生效之前创建的,那么

下面我创建了一个简化的Python代码示例。基本上,程序在图像上生成十字,其位置由一些数据文件指定。然后将图像和画布保存到png文件中。它对数据文件中的不同行重复执行多次(下面我只是使用了一个“pos_列表”进行演示)

我的问题是,为了使用export_to_png,必须允许屏幕更新并显示画布指令中的更改。这意味着我需要使用Kivy时钟。然而。当我安排多个时钟事件时,我无法保证它们会出于某种原因以正确的顺序发生(即使是精心选择的计时)。这是一个问题,因为如果PNG是在画布指令生效之前创建的,那么它们将是不完整的。同样,我的代码的最终目的是创建一个包含所有这些图像的PDF(do_something_with_pages函数是这个函数的占位符),如果这个函数在创建图像之前调用,它将失败

我理解这是一个复杂的问题,但任何关于如何使时钟事件行为,甚至如何在不使用时钟的情况下实现这一点的见解都将不胜感激

from functools import partial
from kivy.app import App
from kivy.core.window import Window
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.clock import Clock
from kivy.graphics import *
from kivy.uix.label import Label

Builder.load_string('''
<NumberLabel>:
    size: 10, 15
    text_size: self.size
    color: 0, 0, 0, 1
    outline_color: 1, 1, 1
    outline_width: 1
    
<AScreen>:
    Button:
        size_hint: 0.2, 0.1
        pos_hint: {"x": 0.4, "y": 0.85}
        on_release:
            root.parent_function(8, '/some/image/file/path.png')
    Image:
        id: an_image
        size_hint: 0.8, 0.8
        pos_hint: {"x": 0.1, "y": 0}
''')

dir_path = '/some/directory'

class NumberLabel(Label):
    pass

Class AScreen(Screen):

    def parent_function(self, pages, path):

        t_interval = 0.7

        for i in range(pages):
            pos_list = [(0.2, 0.3), (0.15, 0.74)] # This is usually a list of 
                                                  # (x, y) relative 
                                                  # coordinates that is read 
                                                  # from a csv file and 
                                                  # varies per page, but here
                                                  # I have set it to 
                                                  # something fixed
            Clock.schedule_once(partial(self.draw_on_image, i, path, pos_list), t_interval * i)
            
        Clock.schedule_once(self.do_something_with_pages, (t_interval + 0.18) * pages)
        
        

    def draw_on_image(self, page, path, pos_list, *args):
        # This function draws a number of canvas objects on the image and 
        # adds a label with a number for each object drawn.
        # It is scheduled with Clock so that the canvas may later be exported 
        # as an image before it is changed by the next iteration of the loop
        self.ids.an_image.source = path
        self.ids.an_image.canvas.after.clear()
        self.ids.an_image.clear_widgets()
        image_pos = self.ids.an_image.pos
        image_width = self.ids.an_image.width
        image_height = self.ids.an_image.height
        obj_num = 0
        for item in pos_list:
            new_pos = (image_pos[0] + item[0] * image_width, image_pos[1] + item[1] * image_height)
            x1, y1 = new_pos[0] - 3, new_pos[1] - 3
            x2, y2 = new_pos[0] + 3, new_pos[1] + 3
            with self.ids.an_image.canvas.after:
                Color(1, 1, 1)
                Line(points=[(x1,y1), (x2,y2)], width=2)
                Line(points=[(x1,y2), (x2,y1)], width=2)
                Color(0, 0, 0)
                Line(points=[(x1,y1), (x2,y2)], width=1)
                Line(points=[(x1,y2), (x2,y1)], width=1)
            obj_num += 1
            l = NumberLabel(pos=(x1,y2), text=str(obj_num))
            self.ids.an_image.add_widget(l)

        Clock.schedule_once(partial(self.export_image, page), 0.002)
        
    def export_image(self, page, *args):
        # This MUST happen after the previous canvas changes show up on 
        # screen but before the next canvas changes are made
        png_path = '{dir}/image{num}.png'.format(dir=dir_path, num=page)
        self.ids.an_image.export_to_png(png_path)
        
    def do_something_with_pages(self, *args):
        # This function does something with all of the saved pngs so MUST 
        # only be called after all of the PNGs have been created
        pass
        
        
class Manager(ScreenManager):
    pass

class MainApp(App):
    def build(self):
        self.title = 'Example'
        self.sm = Manager()
        return self.sm

if __name__ == "__main__":
    app = MainApp()
    app.run()
从functools导入部分
从kivy.app导入应用程序
从kivy.core.window导入窗口
从kivy.lang导入生成器
从kivy.uix.screenmanager导入screenmanager,屏幕
从kivy.clock导入时钟
从kivy.graphics导入*
从kivy.uix.label导入标签
Builder.load_字符串(“”)
:
尺码:10、15
文本大小:self.size
颜色:0,0,0,1
轮廓颜色:1,1,1
轮廓宽度:1
:
按钮:
大小提示:0.2,0.1
pos_提示:{“x”:0.4,“y”:0.85}
发布时:
父函数(8'/some/image/file/path.png')
图片:
id:A_图像
尺寸提示:0.8,0.8
pos_提示:{“x”:0.1,“y”:0}
''')
目录路径='/some/directory'
类别编号标签(标签):
通过
类别A屏幕(屏幕):
def parent_功能(自身、页面、路径):
t_间隔=0.7
对于范围内的i(页):
pos_list=[(0.2,0.3),(0.15,0.74)]这通常是一个
#(x,y)相对的
#读取的坐标
#从csv文件和
#每页不同,但在这里
#我已经设定好了
#固定的东西
时钟。计划一次(部分(在图像、i、路径、位置列表上自行绘制),t\u间隔*i)
时钟。计划一次(self.do\u用页面做某事,(t\u间隔+0.18)*页面)
def在图像上绘制(自身、页面、路径、位置列表、*args):
#此函数用于在图像上绘制多个画布对象,并
#为绘制的每个对象添加带有编号的标签。
#它与时钟一起安排,以便稍后可以导出画布
#在循环的下一次迭代更改之前作为图像
self.ids.an_image.source=路径
self.ids.an_image.canvas.after.clear()
self.ids.an_image.clear_widgets()
image\u pos=self.ids.an\u image.pos
image\u width=self.ids.an\u image.width
image\u height=self.ids.an\u image.height
对象数量=0
对于pos_列表中的项目:
新位置=(图像位置[0]+项目[0]*图像位置宽度,图像位置[1]+项目[1]*图像位置高度)
x1,y1=新位置[0]-3,新位置[1]-3
x2,y2=新位置[0]+3,新位置[1]+3
使用self.ids.an_image.canvas.after:
颜色(1,1,1)
线(点=[(x1,y1),(x2,y2)],宽度=2)
线(点=[(x1,y2),(x2,y1)],宽度=2)
颜色(0,0,0)
线(点=[(x1,y1),(x2,y2)],宽度=1)
线(点=[(x1,y2),(x2,y1)],宽度=1)
对象数+=1
l=NumberLabel(pos=(x1,y2),text=str(obj_num))
self.ids.an_image.add_小部件(l)
时钟计划一次(部分(自导出图像,第页),0.002)
def导出_图像(自身,第页,*args):
#这必须在上一次画布更改显示在
#屏幕,但在进行下一次画布更改之前
png_path='{dir}/image{num}.png'。格式(dir=dir_path,num=page)
self.ids.an_image.export_to_png(png路径)
def do_something_与_页面(self,*args):
#此函数对所有保存的PNG执行某些操作,因此必须
#仅在创建所有PNG后调用
通过
班级经理(屏幕经理):
通过
类主应用程序(应用程序):
def生成(自):
self.title='Example'
self.sm=Manager()
返回self.sm
如果名称=“\uuuuu main\uuuuuuuu”:
app=MainApp()
app.run()

您不需要在代码中过多地使用
Clock.schedule_once()
。您可以对图像进行更改,然后只需调用
Clock.schedule\u once()
即可让kivy更新图像

我已经在下面发布了一个
AScreen
类的修改版本。修改只在一个位置使用
Clock.schedule\u once()
。然后,我添加了
pages
变量,以便向下传递堆栈,以确定何时导出了所有文件。请参见代码中的注释:

class AScreen(Screen):

    def parent_function(self, pages, path):

        t_interval = 0.7

        for i in range(pages):
            pos_list = [(0.2, 0.3), (0.15, 0.74)] # This is usually a list of
                                                  # (x, y) relative
                                                  # coordinates that is read
                                                  # from a csv file and
                                                  # varies per page, but here
                                                  # I have set it to
                                                  # something fixed

            # do not need Clock.schedule_once() here. We are already on main thread
            self.draw_on_image( i, pages, path, pos_list)   # pass pages, so we can track status

    def draw_on_image(self, page, pages, path, pos_list, *args):
        # This function draws a number of canvas objects on the image and
        # adds a label with a number for each object drawn.
        # It is scheduled with Clock so that the canvas may later be exported
        # as an image before it is changed by the next iteration of the loop
        self.ids.an_image.source = path
        self.ids.an_image.canvas.after.clear()
        self.ids.an_image.clear_widgets()
        image_pos = self.ids.an_image.pos
        image_width = self.ids.an_image.width
        image_height = self.ids.an_image.height
        obj_num = 0
        for item in pos_list:
            new_pos = (image_pos[0] + item[0] * image_width, image_pos[1] + item[1] * image_height)
            x1, y1 = new_pos[0] - 3, new_pos[1] - 3
            x2, y2 = new_pos[0] + 3, new_pos[1] + 3
            with self.ids.an_image.canvas.after:
                Color(1, 1, 1)
                Line(points=[(x1,y1), (x2,y2)], width=2)
                Line(points=[(x1,y2), (x2,y1)], width=2)
                Color(0, 0, 0)
                Line(points=[(x1,y1), (x2,y2)], width=1)
                Line(points=[(x1,y2), (x2,y1)], width=1)
            obj_num += 1
            l = NumberLabel(pos=(x1,y2), text=str(obj_num))
            self.ids.an_image.add_widget(l)

        # use Clock.schedule_once() to give main thread a chance to update image
        Clock.schedule_once(partial(self.export_image, page, pages), 0.002)  # pass pages again

    def export_image(self, page, pages, *args):
        # This MUST happen after the previous canvas changes show up on
        # screen but before the next canvas changes are made
        png_path = '{dir}/image{num}.png'.format(dir=dir_path, num=page)
        self.ids.an_image.export_to_png(png_path)
        print('exported', png_path)
        if page == pages - 1:   # use page and pages to determine if all files are done
            self.do_something_with_pages()

    def do_something_with_pages(self, *args):
        print('do something')
        # This function does something with all of the saved pngs so MUST
        # only be called after all of the PNGs have been created
        pass

好吧,受约翰·安德森回答的启发,我设法破解了它。为任何可能需要它的人在下面发布

我认为不可能通过使用单个图像和更新每个“页面”的源代码和画布来实现期望的结果。相反,我为每个“页面”创建了一个单独的图像(在模式视图中),然后导出了e
from functools import partial
from kivy.app import App
from kivy.core.window import Window
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.clock import Clock
from kivy.graphics import *
from kivy.uix.label import Label
from kivy.uix.modalview import ModalView

Builder.load_string('''
<NumberLabel>:
    size: 10, 15
    text_size: self.size
    color: 0, 0, 0, 1
    outline_color: 1, 1, 1
    outline_width: 1
    
<AScreen>:
    Button:
        size_hint: 0.2, 0.1
        pos_hint: {"x": 0.4, "y": 0.85}
        on_release:
            root.parent_function(8, '/some/image/file/path.png')

<AModalView>:
    size_hint: 0.8, 0.8
    pos_hint: {"x": 0.1, "y": 0.1}
    FloatLayout:
        Image:
            id: an_image
''')

dir_path = '/some/directory'

class NumberLabel(Label):
    pass

class AModalView(ModalView):
    pass

Class AScreen(Screen):

    def parent_function(self, pages, path):

        self.popup_list = []

        for i in range(pages):
            pos_list = [(0.2, 0.3), (0.15, 0.74)] # This is usually a list of 
                                                  # (x, y) relative 
                                                  # coordinates that is read 
                                                  # from a csv file and 
                                                  # varies per page, but here
                                                  # I have set it to 
                                                  # something fixed
            self.draw_on_image(i, path, pos_list)
            
        Clock.schedule_once(partial(self.do_something_with_pages, pages), 0)
        

    def draw_on_image(self, page, path, pos_list, *args):
        # This function creates a popup with an image inside it.
        # It then draws a number of canvas objects on the image and 
        # adds a label with a number for each object drawn.
        self.popup_list.append(AModalView())
        self.popup_list[page - 1].open()
        self.popup_list[page - 1].ids.an_image.source = path
        image_pos = self.popup_list[page - 1].ids.an_image.pos
        image_width = self.popup_list[page - 1].ids.an_image.width
        image_height = self.popup_list[page - 1].ids.an_image.height
        obj_num = 0
        for item in pos_list:
            new_pos = (image_pos[0] + item[0] * image_width, image_pos[1] + item[1] * image_height)
            x1, y1 = new_pos[0] - 3, new_pos[1] - 3
            x2, y2 = new_pos[0] + 3, new_pos[1] + 3
            with self.popup_list[page - 1].ids.an_image.canvas.after:
                Color(1, 1, 1)
                Line(points=[(x1,y1), (x2,y2)], width=2)
                Line(points=[(x1,y2), (x2,y1)], width=2)
                Color(0, 0, 0)
                Line(points=[(x1,y1), (x2,y2)], width=1)
                Line(points=[(x1,y2), (x2,y1)], width=1)
            obj_num += 1
            l = NumberLabel(pos=(x1,y2), text=str(obj_num))
            self.popup_list[page - 1].ids.an_image.add_widget(l)
        
    def do_something_with_pages(self, pages, *args):
        # export to png for the image on each popup created, then dismiss all the 
        # popups
        for page in range(pages):
            png_path = '{dir}/image{num}.png'.format(dir=dir_path, num=page)
            self.popup_list[page - 1].ids.an_image.export_to_png(png_path)
            self.popup_list[page - 1].dismiss()
        # Now do something with all the pngs
        
        
class Manager(ScreenManager):
    pass

class MainApp(App):
    def build(self):
        self.title = 'Example'
        self.sm = Manager()
        return self.sm

if __name__ == "__main__":
    app = MainApp()
    app.run()