2025, Oct 02 05:32
Tkinter में Canvas पर इमेज क्यों नहीं दिखती: PhotoImage, PIL और Toplevel रेफरेंस का सही उपयोग
Python Tkinter में Canvas पर इमेज न दिखने का कारण और हल: PhotoImage रेफरेंस को सुरक्षित रखें, Toplevel सबक्लास करें, या PIL इमेज को विजेट एट्रिब्यूट में रखें।
जब आप Tkinter ऐप को मॉड्यूलर बनाते हैं, तो दिखने में साधारण-सा रिफैक्टर Canvas पर इमेज रेंडरिंग तोड़ सकता है। विंडो खुलती है, विजेट बन जाते हैं, लेकिन लोगो कभी दिखता ही नहीं। जड़ कारण Canvas या PIL में नहीं, बल्कि ऑब्जेक्ट के जीवनकाल में है: जिस विंडो ऑब्जेक्ट के पास इमेज की मालिकाना हक है, उसे जीवित नहीं रखा जाता, इसलिए Python उसे PhotoImage संदर्भ के साथ ही गार्बेज-कलेक्ट कर देता है।
समस्या को दोहराना
नीचे न्यूनतम संरचना दी गई है जो असफल सेटअप को दर्शाती है: एक अलग मॉड्यूल में क्लास जो Toplevel बनाती है और Canvas पर इमेज खींचती है, और एक मुख्य लॉन्चर जो विंडो बनाता है पर उसका कोई संदर्भ नहीं रखता।
from tkinter import *
from PIL import ImageTk, Image
class CreateSecretDialog:
def __init__(self, parent_win):
self.parent_win = parent_win
self.top_win = Toplevel(self.parent_win)
self.tk_logo = ImageTk.PhotoImage(Image.open("password_img.png"))
self._setup_shell()
self._build_canvas()
def _setup_shell(self):
self.top_win.title("Add a password")
self.top_win.config(padx=50, pady=50)
def _build_canvas(self):
surface = Canvas(self.top_win, width=205, height=205)
surface.create_image(102, 102, image=self.tk_logo)
surface.grid(column=0, row=0)
from tkinter import *
import child_windows
# बटन क्लिक होने पर इसे चलाएँ
def spawn_dialog():
child_windows.CreateSecretDialog(app_root)
# रूट UI
app_root = Tk()
app_root.title("Password Manager")
app_root.config(padx=15, pady=15)
btn_new = Button(app_root, command=spawn_dialog, text="Add Password")
btn_new.grid(column=0, row=0)
app_root.mainloop()
असल में गड़बड़ी कहाँ होती है
डायलॉग का इंस्टेंस तो बनता है, लेकिन कहीं संग्रहीत न होने के कारण तुरंत ही स्कोप से बाहर हो जाता है। तब Python का गार्बेज कलेक्टर उसे बेझिझक हटा देता है। PhotoImage का संदर्भ उसी अल्पकालिक ऑब्जेक्ट में होता है, इसलिए वह भी कलेक्ट हो जाता है। Tkinter Canvas को तो रखता है, पर आधारभूत इमेज गायब हो चुकी होती है, इसलिए Canvas दिखने के बावजूद उस पर कुछ नजर नहीं आता।
ऐसे समाधान जो इमेज को जीवित रखें
कम-से-कम एक स्थायी संदर्भ जीवित रखकर इसे सुलझाया जा सकता है। UI डिज़ाइन बदले बिना ऐसा करने के तीन सरल तरीके हैं।
सबसे सीधा तरीका है मॉड्यूल-स्तर पर डायलॉग ऑब्जेक्ट को थामे रखना। इससे मालिक ऑब्जेक्ट (और उसका PhotoImage) कलेक्ट होने से बचता है।
# child_windows.py ऊपर जैसा ही रहता है
from tkinter import *
import child_windows
add_secret_view = None # मॉड्यूल-स्तरीय होल्डर
def spawn_dialog():
global add_secret_view
add_secret_view = child_windows.CreateSecretDialog(app_root)
app_root = Tk()
app_root.title("Password Manager")
app_root.config(padx=15, pady=15)
btn_new = Button(app_root, command=spawn_dialog, text="Add Password")
btn_new.grid(column=0, row=0)
app_root.mainloop()
अगर आप ग्लोबल डायलॉग संदर्भ से बचना चाहते हैं, तो इमेज को किसी दीर्घकालिक विजेट से बाँध दें। Canvas (या Toplevel, या यहाँ तक कि root) पर इसे एक एट्रिब्यूट के रूप में सेट करने से ऑब्जेक्ट उतनी देर तक संदर्भित रहता है, जितनी देर वह विजेट मौजूद है।
from tkinter import *
from PIL import ImageTk, Image
class CreateSecretDialog:
def __init__(self, parent_win):
self.parent_win = parent_win
self.top_win = Toplevel(self.parent_win)
self.tk_logo = ImageTk.PhotoImage(Image.open("password_img.png"))
self._setup_shell()
self._build_canvas()
def _setup_shell(self):
self.top_win.title("Add a password")
self.top_win.config(padx=50, pady=50)
def _build_canvas(self):
surface = Canvas(self.top_win, width=205, height=205)
surface.create_image(102, 102, image=self.tk_logo)
surface.grid(column=0, row=0)
# किसी विजेट पर स्थायी संदर्भ रखें
surface.tk_logo = self.tk_logo
# वैकल्पिक:
# self.top_win.tk_logo = self.tk_logo
# self.parent_win.tk_logo = self.tk_logo
एक और साफ़ तरीका है कि डायलॉग को ही Toplevel का सबक्लास बना दें। इस रूप में वह एक सही मायने का विजेट बन जाता है, और हर काम के लिए self का इस्तेमाल स्वाभाविक रहता है। PhotoImage उसी विजेट इंस्टेंस से जुड़ा रहता है जिसे Tkinter मैनेज करता है।
from tkinter import *
from PIL import ImageTk, Image
class CreateSecretDialog(Toplevel):
def __init__(self, parent_win):
super().__init__(parent_win)
self.tk_logo = ImageTk.PhotoImage(Image.open("password_img.png"))
self._setup_shell()
self._build_canvas()
def _setup_shell(self):
self.title("Add a password")
self.config(padx=50, pady=50)
def _build_canvas(self):
pad = Canvas(self, width=205, height=205)
pad.create_image(102, 102, image=self.tk_logo)
pad.grid(column=0, row=0)
वास्तविक प्रोजेक्ट्स में यह क्यों मायने रखता है
जैसे ही Tkinter ऐप एक फ़ाइल से आगे बढ़ता है, ऑब्जेक्ट्स का जीवनकाल साफ़-साफ़ दिखना बंद हो जाता है। कहीं भी संग्रहीत किए बिना कोई ऐसी क्लास बनाना जो विजेट्स पकड़कर रखे, एक सूक्ष्म गलती है—क्योंकि वह साथ में इमेज ऑब्जेक्ट्स को भी ले डूबती है। यह समझ कि PhotoImage तब तक रेफरेंस्ड रहना चाहिए जब तक वह स्क्रीन पर दिख रहा हो, अदृश्य Canvas की घंटों की डिबगिंग बचा देती है। अगर आपका linter ग्लोबल होल्डर के अनिर्दिष्ट होने की शिकायत करता है, तो मॉड्यूल-स्तर पर उसे None से इनिशियलाइज़ कर देना चेतावनी को शांत रखता है और डिज़ाइन भी स्पष्ट रहता है।
मुख्य बातें
जिस ऑब्जेक्ट के पास वे संसाधन हों जिनकी स्क्रीन पर अभी ज़रूरत है, उसे जीवित रखें। या तो डायलॉग इंस्टेंस को किसी दीर्घकालिक वेरिएबल में बनाए रखें, या PhotoImage को Canvas/Toplevel जैसे विजेट के एट्रिब्यूट से बाँध दें, या डायलॉग को Toplevel से इनहेरिट कराएँ और इमेज को इंस्टेंस एट्रिब्यूट के रूप में रखें। इन तीनों तरीकों से इमेज किसी भी अल्पकालिक लोकल स्कोप से अधिक देर तक टिकती है और आपके Canvas पर भरोसेमंद तरीके से रेंडर होती है।