Kivy garden graph: axes are drawn but plot is missing - python

Concerning the following code, which is a subset of my calculation game app, I have two questions:
1) Why is dummy_series not plotted, although the axes (with ticks and labels are drawn)
2) How do I get the quit button to properly exit the app (does it have to remove all widgets from the root widget? Is there an oposite to AppObject.run(), that stops the app? - SOLVED: App.get_running_app().stop()
For the first question, the relevant part of the code is found in the StatisticScreen and the PlotScreen classes. The first one creates the dummy_series, initializes the creation of the graph widget and changes the screen to the PlotScreen. In the PlotScreen class, there is showPlot methods, which is basically copied from the github README.
So far I tried to change the overall background color to white. Both by "canvas before" a white rectangle, and by really changing the background color of the window. Both had no effect (exept the axes and labels were hidden, because they are also white). Then I tried to color the graph differently each time I create it (taken from the same github repo, there is a TestApp in if __name__ == '__main__':). But there is still no graph.
For the second question please consider the changeScreen-method of CalculationRoot. If it is called with quit as the argument, currently it just empties the screen_list and returns False. The idea was to call the callback of the "Back"-Button (key=27,1000). Since closing the App with the "Back"-Button actually works, given the screen_list is empty, I thought I could use this existing process. Also scheduling the keyHandler-method of the app object CalculationApp does not have the desired effect of closing the app.
# Python build-in Modules
import os
import operator # better handling of +, #, *, etc.
import webbrowser # access homepages via the about section
import random # create random math questions
import datetime # for the timer
import itertools # eg for cycling colors
from functools import partial # schedule callback functions that take arguments different from 'dt'
# Kivy
from kivy.lang.builder import Builder
from kivy.app import App
from kivy.core.window import Window
from kivy.utils import platform
from kivy.uix.screenmanager import Screen
from kivy.properties import ObjectProperty, NumericProperty, StringProperty
from kivy.storage.dictstore import DictStore
from kivy.utils import get_color_from_hex as rgb
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.button import Button
from kivy.uix.scrollview import ScrollView
from kivy.clock import Clock
from kivy.uix.label import Label
from kivy.logger import Logger
from kivy.garden.graph import Graph, MeshLinePlot, SmoothLinePlot
# Non-standard python
import numpy as np
###############################################################################
# Constants
###############################################################################
BACKGROUND_COLOR = [0,0,0,0]
TEXT_COLOR = [1,1,1,1]
kv_string = """
#:import Factory kivy.factory.Factory
#:set color_button (0.784, 0.4, 0.216, 1) # brown
#:set color_button_pressed (0.659, 0.3, 0.431, 1) # darker brown
#:set color_background_down '(0.4, 0.4, 0.4, 1)' # purple
<WrappedLabel#Label>:
size_hint_y: None
height: self.texture_size[1] + self.texture_size[1]/2
markup: True
<GridLabel#Label>:
font_size: min(root.width, root.height) * .3
<StatisticsSpinner#Spinner>:
background_color: color_button if self.state == 'normal' else color_button_pressed
background_down: color_background_down
option_cls: Factory.get("SpinnerLabel")
<SpinnerLabel#SpinnerOption>:
background_color: color_button if self.state == 'down' else color_button_pressed
background_down: color_background_down
<CalculationRoot>:
orientation: 'vertical'
cg_screen_manager: cg_screen_manager
statistic_screen: statistic_screen
plot_screen: plot_screen
ScreenManager:
id: cg_screen_manager
StartScreen:
name: 'StartScreen'
StatisticScreen:
id: statistic_screen
name: 'StatisticScreen'
PlotScreen:
id: plot_screen
name: 'PlotScreen'
<StartScreen#Screen>:
BoxLayout:
orientation: 'vertical'
padding: root.width * .02, root.height * .02
spacing: min(root.width, root.height) * .02
WrappedLabel:
text: '[b] Calculation Game [/b]'
font_size: min(root.width, root.height) * .1
Button:
text: 'Statistic'
on_release: app.root.changeScreen(self.text.lower())
Button:
text: 'Quit'
<StatisticScreen#Screen>:
stats_operation_spinner: stats_operation_spinner
stats_difficulty_spinner: stats_difficulty_spinner
stats_num_questions_button: stats_num_questions_button
BoxLayout:
orientation: 'vertical'
padding: root.width * .02, root.height * .02
spacing: min(root.width, root.height) * .02
WrappedLabel:
text: '[b] Statistics [/b]'
halign: 'center'
font_size: min(root.width, root.height) * .1
GridLayout:
size_hint: .9,.4
cols: 2
pos_hint: {'center_x': .5}
GridLabel:
text: 'Operation Type'
StatisticsSpinner:
id: stats_operation_spinner
text: '+'
values: ['+', '-', '*', ':', '%']
GridLabel:
text: 'Difficulty'
StatisticsSpinner:
id: stats_difficulty_spinner
text: '1'
values: ['1','2','3','4']
GridLabel:
text: 'Number of Questions'
Button:
id: stats_num_questions_button
text: '8'
on_release: app.root.statistic_screen.switchNumQuestions(self, self.text)
# on_release: self.text = '16' if self.text == '8' else self.text = '8'
background_color: color_button if self.state == 'normal' else color_button_pressed
Button:
size_hint: 1, .2
text: 'Plot'
on_release: app.root.statistic_screen.showPlot()
font_size: min(root.width, root.height) * .1
<PlotScreen#Screen>:
"""
###############################################################################
# Widgets
###############################################################################
class StatisticScreen(Screen):
""" Selection screen, where you can fix the parameters for
a pot of your statistics.
"""
def __init__(self, *args, **kwargs):
super(StatisticScreen, self).__init__(*args, **kwargs)
def showPlot(self):
""" 'onPlotButtonPress'
callback for the 'plot'-Button on the bottom of StatisticScreen
"""
dummy_series = np.random.randint(1, 10, (12,))
App.get_running_app().root.ids.plot_screen.createKivyPlot(dummy_series)
App.get_running_app().root.changeScreen('plot')
def switchNumQuestions(self, instance, text):
""" 'onNumQuestionsButtonPress'
callback for the 'choose number of questions'-button.
"""
if int(text) == 16:
instance.text = '8'
else:
instance.text = '16'
class PlotScreen(Screen):
def __init__(self, *args, **kwargs):
super(PlotScreen, self).__init__(*args, **kwargs)
self.series = None
self.graph_figure = None # error if uncommented... why? (referred to former name self.canvas)
self.colors = itertools.cycle([rgb('7dac9f'), rgb('dc7062'), rgb('66a8d4'), rgb('e5b060')])
def createKivyPlot(self, series=np.array(range(12))):
graph_theme = {
'label_options': {
'color': rgb('444444'), # color of tick labels and titles
'bold': True},
'background_color': rgb('f8f8f2'), # back ground color of canvas
'tick_color': rgb('808080'), # ticks and grid
'border_color': rgb('808080')} # border drawn around each graph
self.graph_figure = Graph(xlabel='Last 12 games', ylabel='Average Response Time', x_ticks_minor=1,
x_ticks_major=5, y_ticks_major=1,
y_grid_label=True, x_grid_label=True, padding=10,
x_grid=True, y_grid=True, xmin=0, xmax=len(series), ymin=0, ymax=int(1.5*max(series)), **graph_theme)
plot = SmoothLinePlot(mode='line_strip', color=next(self.colors))
plot.points = [(x, series[x]) for x in range(0, len(series))]
self.graph_figure.add_plot(plot)
self.add_widget(self.graph_figure)
def destroy(self):
self.remove_widget(self.graph_figure)
self.graph_figure = None
###############################################################################
# Root Widget
###############################################################################
class CalculationRoot(BoxLayout):
""" Root of all widgets
"""
calculation_screen = ObjectProperty(None)
def __init__(self, *args, **kwargs):
super(CalculationRoot, self).__init__(*args, **kwargs)
self.screen_list = [] # previously visited screens
def changeScreen(self, next_screen):
if self.screen_list == [] or self.ids.cg_screen_manager.current != self.screen_list[-1]:
self.screen_list.append(self.ids.cg_screen_manager.current)
if next_screen == 'start':
self.ids.cg_screen_manager.current = 'StartScreen'
elif next_screen == 'statistic':
self.ids.cg_screen_manager.current = 'StatisticScreen'
elif next_screen == 'plot':
self.ids.cg_screen_manager.current = 'PlotScreen'
elif next_screen == 'quit':
self.screen_list == []
#self.onBackBtnPress() # not working
#Clock.schedule_once(partial(App.get_running_app().keyHandler(27)), 0) # not working
return False
def onBackBtnPress(self):
if self.screen_list:
self.ids.cg_screen_manager.current = self.screen_list.pop()
return True
return False
###############################################################################
# App Object
###############################################################################
class CalculationApp(App):
""" App object
"""
def __init__(self, *args, **kwargs):
super(CalculationApp, self).__init__(*args, **kwargs)
self.use_kivy_settings = False
def keyHandler(self, *args):
key = args[1]
print(key)
if key in (1000, 27):
return self.root.onBackBtnPress()
def post_build_init(self, ev):
if platform == 'android':
pass
win = self._app_window
win.bind(on_keyboard = self.keyHandler)
def build(self):
Builder.load_string(kv_string)
self.bind(on_start=self.post_build_init)
return CalculationRoot()
if __name__ in ('__main__', '__android__'):
CalculationApp().run()

Apparently, there has been a problem with using kivy.garden.graph inside a ScreenManager for some time. According to this issue report, it has been fixed in kivy version v1.10.1.dev0. However, I think you can get around it by adding _with_stencilbuffer=False to your call to Graph().
And to stop the app, you can modify your kv_string in the StartScreen section to include :
Button:
text: 'Quit'
on_release: app.stop()

Related

How to switch text of a label in kivy

I am creating myself a GPA calculator using python kivy and I am trying to switch the text of a label but it is not working. I could use some help. I am only going to show the code that I require to change the text, but if you need it all, I am happy to send it through.
.py file:
from kivy.core.window import Window
from kivy.app import App
from kivy.uix.screenmanager import Screen, ScreenManager
from kivy.uix.dropdown import DropDown
from kivy.uix.button import Button
from kivy.uix.popup import Popup
class main(Screen):
def add_class(self):
sm.current = 'add'
sm.transition.direction = 'left'
def remove_class(self):
sm.current = 'remove'
sm.transition.direction = 'right'
def update(self):
#from the screen that adds the courses
courses = AddClass.courses
marks = []
for key in courses:
marks.append(courses[key])
total = 0.0
for i in marks:
total += i
total /= len(marks)
#print(total)
self.ids.gpa.text = f'{total}/4.0'
class MyApp(App):
def build(self):
#main screen
sm.add_widget(main(name='main'))
#screen that adds the marks
sm.add_widget(AddClass(name='add'))
#screen that removes a certain mark
sm.add_widget(RemoveClass(name='remove'))
sm.current = 'main'
return sm
if __name__ == '__main__':
MyApp().run()
.kv file:
<main>
FloatLayout:
Button:
text: 'Your GPA'
font_size: 50
size_hint: 0.4, 0.4
pos_hint: {'x': 0.3, 'y': 0.6}
id: gpa
background_color: 0, 0, 0
on_release: print(self.text)
Button:
text: 'Remove Class/Course'
font_size: 28
id: remove_class
size_hint: 0.4, 0.4
pos_hint: {'y': 0.1, 'x': 0.05}
on_release: root.remove_class()
Button:
text: 'Add Class/Course'
font_size: 30
id: add_class
size_hint: 0.4, 0.4
pos_hint: {'y': 0.1, 'x': 0.55}
on_release: root.add_class()
Help is much appreciated!
Text of a label can be a kivy property, which can be later changed and since it is a kivy property it will automatically update everywhere. Here is an example of .py
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.properties import StringProperty
import random
class YourWidget(Widget):
random_number = StringProperty()
def __init__(self, **kwargs):
super(YourWidget, self).__init__(**kwargs)
self.random_number = str(random.randint(1, 100))
def change_text(self):
self.random_number = str(random.randint(1, 100))
class YourApp(App):
def build(self):
return YourWidget()
if __name__ == '__main__':
YourApp().run()
and .kv example
<YourWidget>:
BoxLayout:
size: root.size
Button:
id: button1
text: "Change text"
on_release: root.change_text()
Label:
id: label1
text: root.random_number
When you click the button, it will call the change_text() function, which will randomly change the text of the label to a random integer between 1 and 100.

How to keep label from going off the screen in kivy when we type big text?

I'm trying to create a chatbot. As soon as I click the button it creates a new widget but if the input is too long it goes off screen. I'm not able to figure out how to keep all the code in the screen.
Python code
from kivy.app import App
from kivy.uix.textinput import TextInput
from kivy.uix.floatlayout import FloatLayout
from kivy.properties import ObjectProperty
from kivy.uix.label import Label
from kivy.uix.widget import Widget
import time
class MyFL(FloatLayout):
input = ObjectProperty(None)
def btn(self):
self.add_widget(Label( text= '>$' + self.input.text, pos=(-650, 100)))
class MyApp(App):
def build(self):
return MyFL()
if __name__ == "__main__":
MyApp().run()
Kv code
<FloatLayout>:
input:input
TextInput:
id: input
multiline: False
hint_text: "Say Something"
size_hint: 1, 0.05
font_size: 20
Button:
size_hint: 0.05, 0.05
pos_hint: {'center_x': 0.5, 'y': 0.06 }
on_press: root.btn()
Since Label doesn't do line wrapping, I suggest using a readonly TextInput. In your python you can define a custom TextInput:
class MyTextInput(TextInput):
pass
and use it in your btn() method:
def btn(self):
self.add_widget(MyTextInput(text='>$' + self.input.text, pos_hint={'y':0.15}))
Plus a rule in your 'kv` for the new class:
<MyTextInput>:
size_hint: 1, None
height: self.minimum_height
readonly: True
I am not sure if it works perfectly in all case (but I tested, might be ok)
I do in this way:
In main.py
import kivy
from kivy.uix.label import Label
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.clock import Clock
# I use this to load kivy string
# easier for me to manipulate : )
kivy.lang.Builder.load_string("""
#:kivy 2.0.0
<MyFL>:
input: input
TextInput:
id: input
multiline: False
hint_text: "Say Something"
size: root.width, root.height *0.05
font_size: 20
focus: True
on_text_validate: # this allow you to add widget when hit enter in the textinput
root.btn() # by calling the func btn()
# Button:
# size_hint: 0.05, 0.05
# pos_hint: {'center_x': 0.5, 'y': 0.06 }
# on_release:
# root.btn()
""")
class MyFL(Widget): # I changed the class to inherit widget instead of floatlayout, just the positioning is different, you can change this 🤔😅
def btn(self):
label = Label(text = '>$ ' + self.input.text, center = self.center)
label.multiline = True
label.font_size = 25
self.add_widget(label)
def update(self, dt): # newly added #
# pass
for i in self.children: # loop through all children
if i != self.ids.input and i.texture_size[0] > self.width: # if children is label
# see how many line it should split
lines = round(i.texture_size[0] / self.width) + 1
for split in range(lines): # split the text
text = i.text.split(" ")
index = int(len(text)*split / lines)
text.insert(index, "\n")
text = " ".join(text)
i.text = text
class MyApp(App):
main = MyFL()
def build(self):
Clock.schedule_interval(self.main.update, 0.1) # schedule the clock to make update
return self.main
if __name__ == "__main__":
MyApp().run()
Explanations are in the code above, this is not the perfect answer but I hope it helps.

Kivy - how to call object method but not run its __init__

I have a label which when you click on it prints its value. It should also change the label on the first screen. Changing that label works as it should, but when I use the same method from outside the main class it does not work. Because of that I think the problem haws something to do with making new instances of classes instead of updating the ones that are already there. My knowledge of OOP is not good and because of that I can't figure this out. Any help and/or tips would be appreciated.
I tried resolving the problem with solutions from : Instantiate a class but don't call its __init__ method , but could not make it work in my case
The problem starts at line 93/94, then goes to line 45 from then it's clear
from kivy.app import *
from kivy.uix.button import *
from kivy.graphics import *
from kivy.uix.widget import *
from kivy.uix.label import *
from kivy.uix.behaviors import *
from kivy.uix.floatlayout import *
from kivy.uix.boxlayout import *
from kivy.uix.scrollview import ScrollView
from kivy.uix.screenmanager import ScreenManager,Screen
from kivy.core.window import Window
from kivy.lang import Builder
from kivy.uix.gridlayout import GridLayout
from kivy.uix.image import Image
brojPjesmi = 50
def dobijJenegaArtista(kega):
f = "Hi"+ str(x[0])
return f
x = [0]
#---------------------------------------------------------------------------------
class Name(Widget):
def __init__(self, **kwargs):
super(Name, self).__init__(**kwargs)
def changeName(self):
self.nam = "yeah"
self.ids.nameOfSong.text = self.nam[0]
self.ids.artistOfSong.text = str(dobijJenegaArtista(x[0]))
#---------------------------------------------------------------------------------
class FirstWindow(Screen):
def __init__(self, **kwargs):
super(FirstWindow, self).__init__(**kwargs)
self.pos = (0, 0)
self.size = (1,1)
self.z = Name()
self.add_widget(self.z)
def next(self):
if x[0] == brojPjesmi:
x[0] = 0
else:
x[0] = x[0] + 1
self.z.changeName()
def selected(self,p):
x[0] = p - 1 # it puts the value clicked to my counter x, because
# I will send it to next which will add 1 i subtract 1
print(x[0]) # check to see if it is correct
self.next()
def last(self):
if x[0] == 0:
x[0] = brojPjesmi
else:
x[0] = x[0] - 1
self.z.changeName()
#---------------------------------------------------------------------------------
#---------------------------------------------------------------------------------
class SecondWindow(Screen,BoxLayout):
def __init__(self, **kwargs):
super(SecondWindow, self).__init__(**kwargs)
self.d = MyRow()
self.add_widget(self.d)
#---------------------------------------------------------------------------------
g = []
class MyRow(ScrollView):
def __init__(self, **kwargs):
super(MyRow, self).__init__(**kwargs)
self.size_hint=(1,None)
self.size=(Window.width,Window.height)
layout = GridLayout(cols = 1,spacing= 10,size_hint_y = None)
layout.bind(minimum_height = layout.setter('height'))
self.butt = Button(text="gmm",height=40,size_hint_y = None,on_press = lambda x:self.change_screen())
layout.add_widget(self.butt)
for i in range(brojPjesmi):
self.z = OneRow()
u = self.z.make()
layout.add_widget(u)
self.add_widget(layout)
def change_screen(self):
App.get_running_app().root.transition.direction = "right"
App.get_running_app().root.current = 'main'
i = [0]
#---------------------------------------------------------------------------------
class OneRow(ButtonBehavior,GridLayout):
def mm(self):
print(self.i)
self.z = MyRow
self.z.change_screen(self)
self.man = FirstWindow()
self.man.selected(self.i)
def make(self):
i[0] = i[0] + 1
self.i = i[0]
self.cols = 3
self.rows = 1
self.height = 25
self.size_hint_y = None
self.on_release = self.mm
self.Treci = Label(text = str(""),size_hint_y= None,size_hint_x =.1,height=25)
self.prviDio = Label(text = str(self.i),size_hint_y= None,height=25)
self.drugiDio = Label(text = str(self.i*2),size_hint_y= None,height=25)
self.add_widget(self.Treci)
self.add_widget(self.prviDio)
self.add_widget(self.drugiDio)
return self
#---------------------------------------------------------------------------------
class langApp(App):
def build(self):
return kv
#---------------------------------------------------------------------------------
class WindowManager(ScreenManager):
pass
kv = Builder.load_file("nu.kv")
if __name__ == '__main__':
langApp().run()
.kv file
#:kivy 1.11.1
WindowManager:
FirstWindow:
SecondWindow:
<FirstWindow>:
name: "main"
FloatLayout:
pos: 0,0
size: root.width,root.height
Button:
on_press: root.last()
text: 'Last'
pos_hint: {'x':.25,'y':.1}
size_hint: .1,.1
Button:
on_release:
text: 'Stop'
pos_hint: {'x':.45,'y':.1}
size_hint: .1,.1
Button:
on_press: root.next()
text: 'Next'
pos_hint: {'x':.65,'y':.1}
size_hint: .1,.1
Button:
on_release:
app.root.current = "second"
root.manager.transition.direction = "left"
text: 'songs'
pos_hint: {'x':.8,'y':.8}
size_hint: .05,.05
<Name>:
FloatLayout:
pos: 0,0
size: root.width,root.height
pos_hint_x: None
pos_hint_y: None
Label:
id: nameOfSong
text: "Choose a song"
font_size: 20
size_hint: None, None
pos_hint: {'x': 0.435, 'y': 0.25}
Label:
id: artistOfSong
text: ""
font_size: 40
size_hint: None, None
pos_hint: {'x': 0.435, 'y': 0.20}
<SecondWindow>:
name: "second"
PS: The code should: when you press next or last it should update the counter and the screen with the hi+counter(it's just a placeholder), after you click the button in the upper right corner it should list the numbers(also a placeholder usually it's songs and artists) and when you click on a number it should take you to the first screen(works until this point) and then change to label to the number you clicked
The number is also printed for debugging purposes

How to schedule the kivy camera to read the texture continuously?

I have been trying to create a kivy camera scanner from a number of sources (I would use the zbarcam if I could, but the garden.xcamera module will not import, therefore I am trying to create something similar).
Problem
The problem is the camera does not read or update the texture continuously nor is there a way that I can find to capture frame-for-frame from the camera. This means I only get the texture on initialization.
Tried
Firstly, I have tried scheduling an event that will update the texture instance every 0.5 seconds. I could not get the texture instance of the camera because there is some delay in the camera to load, which caused an error.
Secondly, I created an on_texture event in my kv string, but it only reads the texture on initialization.
Thirdly, I tried binding the on_texture event a bit later in the python script, by creating a binding function and calling it as a scheduled event. It did not even get the instance.
Fourthly, I created_triggers and ask_update(callbacks) to the _on_texture() event, but again the script loads to fast before the camera can instantiate crashing the script.
Fifthly, I noticed there is a kivy.core.video module that contains a on_frame attribute. Did re-write my script to use it in conjunction with the kivy.uix.video module, but noticed that the video cannot run without first loading a video file.
Code
import kivy
import gi
kivy.require('1.11.1')
gi.require_version('Gst', '1.0')
from collections import namedtuple
from PIL import Image
from kivy.app import App
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import ListProperty, ObjectProperty
from kivy.uix.camera import Camera
import time
from gi.repository import Gst
import pyzbar.pyzbar
from kivy.uix.modalview import ModalView
Builder.load_string('''
#: import Window kivy.core.window.Window
<ScanPreview>:
auto_dismiss: False
size_hint_x: 0.6
size_hint_y: None
height: Window.height / 9
pos_hint: {'top':0.7, 'x': 0.1}
background_normal: ''
background_color: (1, 1, 1, 0)
background: 'white.png'
Label:
id: sc_data
text: 'See me...'
<ScanCamera>:
orientation: 'vertical'
The_Camera:
id: camera
resolution: root.resolution
on_texture: root._on_texture(camera)
ToggleButton:
text: 'Stop'
on_press: camera.play = not camera.play
size_hint_y: None
height: '48dp'
''')
class ScanPreview(ModalView):
pass
class The_Camera(Camera):
pass
class ScanCamera(BoxLayout):
resolution = ListProperty([640, 480])
symbols = ListProperty([])
code_types = ListProperty(set(pyzbar.pyzbar.ZBarSymbol))
cam_cam = ObjectProperty(The_Camera())
the_preview = ObjectProperty(ScanPreview())
Symb = namedtuple('Symb', ['type','data'])
def __init__(self, **kwargs):
super(ScanCamera, self).__init__(**kwargs)
self.cam_cam.play = True
def _on_texture(self, instance):
#source: https://github.com/kivy-garden/garden.zbarcam/blob/develop
#/zbarcam/zbarcam.py
print(instance)
if not instance.texture == None:
print(instance.texture)
self.symbols = self._detect_qrcode_frame(
texture=instance.texture, code_types=self.code_types)
def _detect_qrcode_frame(cls, texture, code_types):
image_data = texture.pixels
size = texture.size
#source: https://github.com/kivy-garden/garden.zbarcam/blob/develop
#/zbarcam/zbarcam.py
# Fix for mode mismatch between texture.colorfmt and data returned
#by
# texture.pixels. texture.pixels always returns RGBA, so that
#should
# be passed to PIL no matter what texture.colorfmt returns. refs:
# https://github.com/AndreMiras/garden.zbarcam/issues/41
pil_image = Image.frombytes(mode='RGBA', size=size,
data=image_data)
symbols = []
print(pil_image)
print(size)
print(texture.tex_coords)
print(texture.target)
codes = pyzbar.pyzbar.decode(pil_image, symbols=code_types)
for code in codes:
symbol = CameraClick.Symb(type=code.type, data=code.data)
symbols.append(symbol)
print(symbols)
return symbols
class TestCamera(App):
title = 'Scan Camera'
def build(self):
return ScanCamera()
def on_stop(self):
cc = The_Camera()
print('Stop')
cc.play = False
def on_pause(self):
return True
def on_resume(self):
pass
TestCamera().run()
Desired result
The camera's texture must continuously update, which will allow the pyzbar and PIL module to decode the texture?
I don't know if this is how its done, but as I answered my own question I am posting the answer and marking it as such.
Answer
So I managed to solve my question by using the example code here: https://kivy-fork.readthedocs.io/en/latest/_modules/kivy/uix/camera.html and mixing in some functions of zbarcam.
By using self.canvas.ask_update() in the on_texture call it updates the texture. I have added to the code and it now updates the texture continuously and prints the barcode to a togglebutton. At the moment I have just tested it on Ubuntu Bionic Beaver. Will test it on android this weekend.
Answer code
import kivy
import gi
kivy.require('1.11.1')
gi.require_version('Gst', '1.0')
from collections import namedtuple
from PIL import Image as Img
from kivy.app import App
from gi.repository import Gst
import pyzbar.pyzbar
from kivy.uix.image import Image
from kivy.core.camera import Camera as CoreCamera
from kivy.properties import NumericProperty, ListProperty, \
BooleanProperty, ObjectProperty
from kivy.lang import Builder
from kivy.uix.floatlayout import FloatLayout
from kivy.core.window import Window
Builder.load_string('''
#: import Window kivy.core.window.Window
<ScanCamera>:
<BarcWin>:
ActionBar:
pos_hint: {'top': 1, 'right': 1}
color: (1,1,1,1)
canvas.before:
Color:
rgba: (0,0,0,1)
Rectangle:
pos: self.pos
size: self.size
ActionView:
use_separator: True
ActionPrevious:
title: ''
with_previous: False
app_icon: ''
ActionButton:
color: (0,0,0,1)
background_normal: ''
Image:
source: 'gear_2.png'
center_y: self.parent.center_y
center_x: self.parent.center_x
size: self.parent.width /1.7, self.parent.height/ 1.7
allow_stretch: True
ActionButton:
color: (0,0,0,1)
size_hint_x: 0.09
background_normal: ''
Image:
source: 'dustbin_backgrnd_792521.png'
center_y: self.parent.center_y
center_x: self.parent.center_x
size: self.parent.width /1.7, self.parent.height/ 1.7
allow_stretch: True
ScanCamera:
pos_hint: {'top': 0.9, 'right': 1}
size_hint: [1, 0.8]
canvas.before:
PushMatrix
Rotate:
angle: 0
origin: self.center
canvas.after:
PopMatrix
Line:
width: 2.
rectangle: (self.x + 40, self.y + 40, self.width/1.1, self.height/1.12)
ToggleButton:
id: show_bcode
pos_hint: {'bottom': 1, 'right': 1}
size_hint: [1, 0.1]
color: (1,1,1,1)
background_color: (0,0,0,0)
background_normal: ''
canvas.before:
Color:
rgba: (.18,.36,.61,1) if self.state=='down' else (0,0,0,1)
Rectangle:
pos: self.pos
size: self.size
text: 'Hier kom die barcode...'
''')
class BarcWin(FloatLayout):
cam_cam = ObjectProperty(None)
def __init__(self, **kwargs):
super(BarcWin, self).__init__(**kwargs)
self.cam_cam = ScanCamera()
def accept_in(self):
print('In')
def accept_out(self):
print('Out')
class ScanCamera(Image):
play = BooleanProperty(True)
index = NumericProperty(-1)
resolution = ListProperty([Window.width, Window.height])
symbols = ListProperty([])
code_types = ListProperty(set(pyzbar.pyzbar.ZBarSymbol))
Symb = namedtuple('Symb', ['type','data'])
app_ini_ = ObjectProperty(None)
got_bcode = BooleanProperty(False)
def __init__(self, **kwargs):
self._camera = None
super(ScanCamera, self).__init__(**kwargs)
if self.index == -1:
self.index = 0
on_index = self._on_index
fbind = self.fbind
fbind('index', on_index)
fbind('resolution', on_index)
on_index()
self.app_ini_ = App.get_running_app()
def on_tex(self, *l):
self.canvas.ask_update()
if not self.texture == None:
self.symbols = self._detect_qrcode_frame(texture=self.texture, code_types=self.code_types)
if not self.symbols == []:
for s in self.symbols:
if s.data:
if s.data.decode('utf-8') != "":
self.app_ini_.root.ids.show_bcode.text = s.data.decode('utf-8')
def _on_index(self, *largs):
self._camera = None
if self.index < 0:
return
if self.resolution[0] < 0 or self.resolution[1] < 0:
return
#first init of corecamera object
self._camera = CoreCamera(index=self.index,
resolution=self.resolution, stopped=True)
#when camera loads call _camera_loaded method to bind corecamera method with uix.image texture
self._camera.bind(on_load=self._camera_loaded)
if self.play:
self._camera.start()
self._camera.bind(on_texture=self.on_tex)
def _camera_loaded(self, *largs):
#bind camera texture with uix.image texture that is still equal to None
self.texture = self._camera.texture
def on_play(self, instance, value):
if not self._camera:
return
if value:
self._camera.start()
else:
self._camera.stop()
def _detect_qrcode_frame(self, texture, code_types):
if not self.got_bcode:
image_data = texture.pixels
size = texture.size
# Fix for mode mismatch between texture.colorfmt and data returned by
# texture.pixels. texture.pixels always returns RGBA, so that should
# be passed to PIL no matter what texture.colorfmt returns. refs:
# https://github.com/AndreMiras/garden.zbarcam/issues/41
pil_image = Img.frombytes(mode='RGBA', size=size,
data=image_data)
bcode = []
codes = pyzbar.pyzbar.decode(pil_image, symbols=code_types)
#print(pil_image, type(pil_image), dir(pil_image))
if codes != []:
for code in codes:
symbol = self.Symb(type=code.type, data=code.data)
bcode.append(symbol)
return bcode
else:
self.got_bcode = False
return []
class TestCamera(App):
title = 'Scan Camera'
def build(self):
return BarcWin()
def on_stop(self):
cc = ScanCamera()
print('Stop')
cc._camera.stop()
def on_pause(self):
return True
def on_resume(self):
pass
TestCamera().run()

How to Periodically Update Parent (Screen) Class UI from Child (BoxLayout) Class (Python with Kivy)

Goal:
Periodic update of parent (screen) class / UI from child (boxlayout) class. Theconf2.dat is occasionally updated (from various other screens), and I want the UI to update every 5 seconds or so by re-running this class.
Latest code update:
In the __init__ function, I have Clock.schedule_interval(self.create_button, 1), which should cause the create_button function to rerun every second.
At the top of the create_button function, I have self.box_share.clear_widgets(), which should clear all the widgets so they can be repopulated (per the instructions outlined further down the create_button function).
Action:
I run the code
I navigate to NoTags screen by clicking the button with text title 'updating sequence'
I make changes to buttons that were dynamically created under scrollview by clicking on them. They successfully change color. This information is written to the conf2.dat file.
I navigate to SequenceScreen screen by first clicking 'home' button, then clicking 'sequence display' button. This SequenceScreen screen is the screen I wish to have updated to reflect the changes made to conf2.dat file.
Result:
UI associated withSequenceScreen(Screen) class still does not update per changes made from UI associated with NoTags(Screen) class.
However, when I restart the app altogether, I find the SequenceScreen UI successfully updated.
Suspicion:
I'm just one line of code away from getting this UI to update properly.
Python Code:
from kivy.app import App
# kivy.require("1.10.0")
from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen, FadeTransition
from kivy.uix.textinput import TextInput
from kivy.uix.gridlayout import GridLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.properties import NumericProperty
from kivy.clock import Clock
from kivy.uix.widget import Widget
from kivy.uix.scrollview import ScrollView
#from kivy.base import runTouchApp
from kivy.properties import StringProperty, ObjectProperty, NumericProperty
from kivy.storage.dictstore import DictStore
import pickle
import datetime, threading
import time
from kivy.clock import mainthread
class BackHomeWidget(Widget):
pass
class SequenceBoxLayout_NoEdits(BoxLayout):
box_share = ObjectProperty()
config_file = DictStore('conf2.dat')
def __init__(self, **kwargs):
super(SequenceBoxLayout_NoEdits, self).__init__(**kwargs)
self.orientation = "vertical"
Clock.schedule_interval(self.create_button, 1)
def create_button(self, *args):
self.box_share.clear_widgets()
top_button_share = 1.1
color = (.4, .4, .4, 1)
for i in range(25):
top_button_share -= .4
id_ = "part" + str(i + 1)
if self.config_file.exists(id_):
btn_color = self.config_file[id_]["background_color"]
else:
self.config_file.put(id_, background_color=color)
btn_color = color
button_share = Button(background_normal='',
background_color=btn_color,
id=id_,
pos_hint={"x": 0, "top": top_button_share},
size_hint_y=None,
height=60,
font_size = 30,
text= str(i+1)
self.box_share.add_widget(button_share)
#Clock.schedule_interval(self.parent.ids.updatedisplay.create_button(self, *args) , 1)
#self.parent.ids.updatedisplay.create_button(self, *args)
class SequenceBoxLayout_NoTag(BoxLayout):
box_share = ObjectProperty()
config_file = DictStore('conf2.dat')
def __init__(self, **kwargs):
super(SequenceBoxLayout_NoTag, self).__init__(**kwargs)
self.orientation = "vertical"
Clock.schedule_once(self.create_button)
def create_button(self, *args):
df = pd.read_excel("Test.xlsx","Sheet1")
parts = df['parts'].values.tolist()
top_button_share = 1.1
color = (.4, .4, .4, 1)
for i in range(len(parts)):
top_button_share -= .4
id_ = "part" + str(i + 1)
if self.config_file.exists(id_):
btn_color = self.config_file[id_]["background_color"]
else:
self.config_file.put(id_, background_color=color)
btn_color = color
button_share = Button(background_normal='',
background_color=btn_color,
id=id_,
pos_hint={"x": 0, "top": top_button_share},
size_hint_y=None,
height=60,
font_size = 30,
text= str(i+1)+ ". " + str(parts[i]))
if self.parent.name == 'notags':
button_share.bind(on_press=self.update_buttons_notag)
self.box_share.add_widget(button_share)
def update_buttons_notag(self, button):
button.background_color = 0.86,0.54,0.04,1
self.config_file.put(button.id, background_color=(0.86,0.54,0.04,1))
class MainScreen(Screen):
pass
class AnotherScreen(Screen):
pass
class SequenceScreen(Screen):
pass
class NoTags(Screen):
pass
class ScreenManagement(ScreenManager):
pass
presentation = Builder.load_file("updatelistexample.kv")
class MainApp(App):
def build(self):
return presentation
if __name__ == "__main__":
MainApp().run()
KV Code:
#: import FadeTransition kivy.uix.screenmanager.FadeTransition
ScreenManagement:
transition: FadeTransition()
MainScreen:
AnotherScreen:
NoTags:
SequenceScreen:
<SmallNavButton#Button>:
font_size: 32
size: 125, 50
color: 0,1,0,1
<BigButton#Button>:
font_size: 40
size_hint: 0.5, 0.15
color: 0,1,0,1
<BackHomeWidget>:
SmallNavButton:
on_release: app.root.current = "main"
text: "Home"
pos: root.x, root.top - self.height
<MainScreen>:
name: "main"
FloatLayout:
BigButton:
on_release: app.root.current = "notags"
text: "updating sequence"
pos_hint: {"x":0.25, "top": 0.4}
BigButton:
on_release: app.root.current = "sequence"
text: "sequence display"
pos_hint: {"x":0.25, "top": 0.7}
<AnotherScreen>:
name: "newgarage"
<NoTags>:
name: "notags"
SequenceBoxLayout_NoTag:
BackHomeWidget:
FloatLayout:
BigButton:
text: "Select Parts w/o Tags"
pos_hint: {"x":0.5, "top": 0.6}
background_normal: ''
background_color: (0.4,0.4,0.4,1)
<SequenceBoxLayout_NoEdits>:
box_share: box_share
ScrollView:
GridLayout:
id: box_share
cols: 1
size_hint_y: None
size_hint_x: 0.5
spacing: 5
padding: 130
height: self.minimum_height
canvas:
Color:
rgb: 0, 0, 0
Rectangle:
pos: self.pos
size: self.size
<SequenceBoxLayout_NoTag>:
box_share: box_share
ScrollView:
GridLayout:
id: box_share
cols: 1
size_hint_y: None
size_hint_x: 0.5
spacing: 5
padding: 130
height: self.minimum_height
canvas:
Color:
rgb: 0, 0, 0
Rectangle:
pos: self.pos
size: self.size
<SequenceScreen>:
name: "sequence"
SequenceBoxLayout_NoEdits:
id: updatedisplay
BackHomeWidget:
Credit:
Based on advice provided by #Tshirtman in the comments thread of the posted question...
Summary:
The problem with the code had to do with the fact that I had two different DictStore pointing to the same file, which was tripping up the communication between both classes.
The solution was to instead use only one DictStore and define that variable in the App class, then reference that particular variable in the child classes [using App.get_running_app()], like so:
Define config_file in App class:
class MainApp(App):
config_file = DictStore('conf2.dat')
def build(self):
return presentation
Reference App variable in child classes:
class SequenceBoxLayout_NoEdits(BoxLayout):
...
def __init__(self, **kwargs):
super(SequenceBoxLayout_NoEdits, self).__init__(**kwargs)
self.orientation = "vertical"
Clock.schedule_interval(self.create_button, 1)
def create_button(self, *args):
self.box_share.clear_widgets()
app = App.get_running_app()
...
for i in range(len(parts)):
...
if app.config_file.exists(id_):
btn_color = app.config_file[id_]["background_color"]
else:
app.config_file.put(id_, background_color=color)
...
...
class SequenceBoxLayout_NoTag(BoxLayout):
...
def __init__(self, **kwargs):
super(SequenceBoxLayout_NoTag, self).__init__(**kwargs)
self.orientation = "vertical"
Clock.schedule_once(self.create_button)
def create_button(self, *args):
...
app = App.get_running_app()
...
for i in range(len(parts)):
...
if app.config_file.exists(id_):
btn_color = app.config_file[id_]["background_color"]
else:
app.config_file.put(id_, background_color=color)
...
...
def update_buttons_notag(self, button):
app = App.get_running_app()
...
app.config_file.put(button.id, background_color=(0.86,0.54,0.04,1))
Clock has a schedule_interval method, which works much like the schedule_once one, but will be called every n seconds instead of just once after n seconds.
So you could certainly just change that call in __init__ and start the create_button by calling self.box_share.clear_widgets() to remove widgets from the previous call before re-creating them.
This might be a bit wasteful, if you find yourself recreating a lot of widgets even if nothing changed, so you could add some logic to first check if the data didn't change, or even if it did, just reuse the old buttons if possible.
box = self.box_share
old_buttons = box.children[:]
box.clear_widgets()
# [your logic about computing the list of parts]
for i, p in enumerate(parts): # this is much better than doing range(len(parts))
# [the logic to get the content of the button]
if old_buttons: # check there are still buttons in the list of old buttons
btn = old_buttons.pop()
else:
btn = Button(
background_normal='',
background_color=btn_color,
pos_hint={"x": 0, "top": top_button_share},
# etc, all the things that are common to all your buttons
# but really, hardcoding them like this is a bit painful,
# you should use a subclass of button so you can style it
# in a kv rule so it would apply to all of them directly
)
btn.id=id_
btn.text = "{}. {}".format(i+1, p)
btn.pos_hint["top"] = top_button_share
# re-apply any other property that might have changed for this part
box.add_widget(btn)
But this logic is quite a common one, actually, and there are other things you can do to improve things in even more situations, though that's quite some work.
Thankfully, you are not the first one to need such thing, and we have been blessed with the wonderful RecycleView, which automates all of this and more, you just need to feed it a data directory, and it'll create the necessary widgets to fill the visible part of the scrollview if there is enough widgets to warrant scrolling, and automatically update when data changes, and when you scroll to see different parts of the list. I encourage you to check it, yourself. but the end result would certainly be something like.
<PartButton#Button>:
id_: None
part: ''
text: '{}. {}'.format(self.id, self.part)
<SequencedBoxLayout#Recycleview>:
parts: self.get_parts()
viewclass: 'PartButton'
data:
[
{
id_: i,
part: part
} for i, p in enumerate(self.parts or [])
]
RecycleBoxLayout:

Categories

Resources