В настоящее время я разрабатываю приложение PyGTK, состоящее из трех связанных Gtk.TreeView
. Выбор элемента в текстовом представлении i
вызовет перезагрузку содержимого в текстовом представлении i+1
. Но по какой-то причине я получаю слишком много обратных вызовов выбор-изменение к текстовому представлению i+1
(по одному для каждой записи), когда текстовое представление i+1
имеет один выбор, и я меняю текстовое представление выделения i
.
Обратите внимание, что я вызываю, например, text_view.get_selection (). Unselect_all () и store.clear () для i+1
и i+2
, когда я изменяю текстовое представление выделения i
.
Это связано с тем, что я использую несколько вариантов выбора? Стоит ли отменить выбор только выбранных в данный момент?
Код следует:
#!/usr/bin/env python3
# command line interface (CLI) parsing
import requests
import argparse
import datetime
import time
import datetime
from pprint import pprint
# graphical user interface (GUI)
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
from gi.repository import GObject
from gi.repository import GLib
from gi.repository.GdkPixbuf import Pixbuf, Colorspace # for images
from gi.repository import Gio
from io import BytesIO
# DICOM
from dicomweb_client.api import DICOMwebClient
# this directory
def try_get_attr(data, tag, default_value=None):
'''Try get attribute indexed by TAG in DICOM data DATA.'''
try:
value = data[tag]['Value'][0]
if data[tag]['vr'] == 'US': # unsigned int
return value # return integer
elif data[tag]['vr'] == 'DA': # date
return datetime.date(int(value[0 : 4]),
int(value[4 : 6]),
int(value[6 : 8]))
elif data[tag]['vr'] == 'TM': # time (of day)
return datetime.time(int(value[0 : 2]),
int(value[2 : 4]),
int(value[4 : 6]))
else:
return value # as is
except:
return default_value
def try_get_attr_alphabetic(data, tag, default_value=None):
try:
return try_get_attr(data, tag, default_value)['Alphabetic']
except:
return default_value
class StudyStore(Gtk.ListStore):
'''Gtk.ListStore of DICOM studies (model).'''
def __init__(self, client):
Gtk.ListStore.__init__(self, str, str, str, str, str) # TODO GDate, GTime
self.client = client
studies = self.client.search_for_studies()
for study in studies:
study_instance_uid = try_get_attr(study, '0020000D') # TODO wrap in DicomUID when 'vr': 'UI'
if study_instance_uid is None:
continue # skip because http://dev-demo.sectra.se contains lots of such entries
study_name = try_get_attr(study, '00100020') # TODO decode string when 'vr': 'LO'
person_name = try_get_attr_alphabetic(study, '00100010') # TODO decode Person Name string when 'vr': 'PN'
study_date = str(try_get_attr(study, '00080020')) # TODO GDate
study_time = str(try_get_attr(study, '00080030')) # TODO GTime
# studies_id = try_get_attr(study, '00020010') # TODO 'vr' : 'UI'. same for all elements in `studies`
# study = client.retrieve_study(study_instance_uid) # retrieve same thing again
self.append((study_instance_uid, study_name, person_name, study_date, study_time))
class SeriesStore(Gtk.ListStore):
'''Gtk.ListStore of DICOM serieses (model).'''
def __init__(self, client):
Gtk.ListStore.__init__(self, str, str)
self.client = client
def update_using_study(self, study_instance_uid):
self.study_instance_uid = study_instance_uid
self.serieses = self.client.search_for_series(study_instance_uid)
for series in self.serieses:
series_instance_uid = try_get_attr(series, '0020000E') # Series Instance UID
series_modality = try_get_attr(series, '00080060') # Modality being either CR, OT, MR, XA, CT, US
self.append((series_instance_uid, series_modality))
class InstanceStore(Gtk.ListStore):
'''Gtk.ListStore of DICOM instances (model).'''
def __init__(self, client):
Gtk.ListStore.__init__(self, str, str)
self.client = client
def update_using_study_and_series(self, study_instance_uid, series_instance_uid):
instances = self.client.search_for_instances(study_instance_uid,
series_instance_uid)
for instance in instances:
sop_class_uid = try_get_attr(instance, '00080016') # SOP Class UID
try:
sop_name = SOP_UID_NAMES[sop_class_uid] # lookup SOP name from UID
except:
sop_name = sop_class_uid # if fails use UID
sop_instance_uid = try_get_attr(instance, '00080018') # SOP Instance UID
self.append((sop_instance_uid, sop_name))
class DICOMWindow(Gtk.Window):
def __init__(self, title="DICOM Viewer"):
self.client = DICOMwebClient(url="https://dicomcloud.azurewebsites.net",
qido_url_prefix="qidors",
wado_url_prefix="wadors",
stow_url_prefix="stowrs")
# construct top window
Gtk.Window.__init__(self,
title=title,
vexpand=True, hexpand=True,
border_width=0)
# create top grid
self.grid = Gtk.Grid(vexpand=True, hexpand=True,
column_homogeneous=True,
row_homogeneous=True)
self.add(self.grid) # add it to parent window
# STUDIES
self.study_store = StudyStore(client=self.client)
self.current_study_filter = None
self.study_filter = self.study_store.filter_new()
self.study_filter.set_visible_func(self.study_view_filter_func)
self.study_view = Gtk.TreeView.new_with_model(self.study_filter)
self.study_selection = self.study_view.get_selection()
self.study_selection.set_mode(Gtk.SelectionMode.MULTIPLE)
self.study_selection.connect("changed", self.on_study_view_selection_changed)
for i, column_title in enumerate(["Study UID", "Study Name", "Person Name", "Study Date", "Study Time",]):
if i == 0:
continue # skip "Study UID"
renderer = Gtk.CellRendererText()
column = Gtk.TreeViewColumn(column_title, renderer, text=i)
self.study_view.append_column(column)
self.study_view_scroller = Gtk.ScrolledWindow(vexpand=True,
hexpand=True)
self.study_view_scroller.add(self.study_view)
self.grid.attach(self.study_view_scroller,
left=0, top=0, width=1, height=1)
# SERIESES
self.series_store = SeriesStore(client=self.client)
self.current_series_filter = None
self.series_filter = self.series_store.filter_new()
self.series_filter.set_visible_func(self.series_view_filter_func)
self.series_view = Gtk.TreeView.new_with_model(self.series_filter)
self.series_selection = self.series_view.get_selection()
self.series_selection.set_mode(Gtk.SelectionMode.MULTIPLE)
self.series_selection.connect("changed", self.on_series_view_selection_changed)
for i, column_title in enumerate(["Series UID", "Series Modality"]):
if i == 0:
continue # skip "Series UID"
renderer = Gtk.CellRendererText()
column = Gtk.TreeViewColumn(column_title, renderer, text=i)
self.series_view.append_column(column)
self.series_view_scroller = Gtk.ScrolledWindow(vexpand=True,
hexpand=True)
self.series_view_scroller.add(self.series_view)
self.grid.attach(self.series_view_scroller,
left=1, top=0, width=1, height=1)
# INSTANCES
self.instance_store = InstanceStore(client=self.client)
self.current_instance_filter = None
self.instance_filter = self.instance_store.filter_new()
self.instance_filter.set_visible_func(self.instance_view_filter_func)
self.instance_view = Gtk.TreeView.new_with_model(self.instance_filter)
self.instance_selection = self.instance_view.get_selection()
self.instance_selection.set_mode(Gtk.SelectionMode.MULTIPLE)
self.instance_selection.connect("changed", self.on_instance_view_selection_changed)
for i, column_title in enumerate(["SOP Instance UID", "SOP Class"]):
if i == 0:
continue # skip "SOP Instance UID"
renderer = Gtk.CellRendererText()
column = Gtk.TreeViewColumn(column_title, renderer, text=i)
self.instance_view.append_column(column)
self.instance_view_scroller = Gtk.ScrolledWindow(vexpand=True,
hexpand=True)
self.instance_view_scroller.add(self.instance_view)
self.grid.attach(self.instance_view_scroller,
left=2, top=0, width=1, height=1)
self.image_view = Gtk.Image()
self.image_view_scroller = Gtk.ScrolledWindow(vexpand=True,
hexpand=True)
self.image_view_scroller.add(self.image_view)
self.grid.attach(self.image_view_scroller,
left=0, top=1, width=3, height=3)
self.show_all()
def on_study_view_selection_changed(self, selection):
print('on_study_view_selection_changed: selection_count=', selection.count_selected_rows())
# needs to be called in this order
self.clear_current_image_and_pixel_data()
self.clear_current_instance_store_and_view()
self.clear_current_series_store_and_view()
model, treeiter = selection.get_selected_rows()
for row_index in treeiter:
self.study_instance_uid = model[row_index][0]
self.series_store.update_using_study(study_instance_uid=self.study_instance_uid)
# TODO visually present:
# study_metadata = self.client.retrieve_study_metadata(self.study_instance_uid)
def on_series_view_selection_changed(self, selection):
print('on_series_view_selection_changed: selection_count=', selection.count_selected_rows())
# needs to be called in this order
self.clear_current_image_and_pixel_data()
self.clear_current_instance_store_and_view()
model, treeiter = selection.get_selected_rows()
for row_index in treeiter:
self.series_instance_uid = model[row_index][0]
self.instance_store.update_using_study_and_series(study_instance_uid=self.study_instance_uid,
series_instance_uid=self.series_instance_uid)
# TODO visually present:
# series_metadata = self.client.retrieve_series_metadata(self.study_instance_uid,
# self.series_instance_uid)
def on_instance_view_selection_changed(self, selection):
print('on_instance_view_selection_changed: selection_count=', selection.count_selected_rows())
# needs to be called in this order
self.clear_current_image_and_pixel_data()
model, treeiter = selection.get_selected_rows()
for row_index in treeiter:
self.sop_instance_uid = model[row_index][0]
# TODO visually present more parts of:
instance_metadata = self.client.retrieve_instance_metadata(study_instance_uid=self.study_instance_uid,
series_instance_uid=self.series_instance_uid,
sop_instance_uid=self.sop_instance_uid)[0]
self.image_height = try_get_attr(instance_metadata, '00280010') # rows
self.image_width = try_get_attr(instance_metadata, '00280011') # columns
self.image_bits_per_pixel = try_get_attr(instance_metadata, '00280100') # bits per pixel
self.frame_count = try_get_attr(instance_metadata, '00280008', '1') # only present for multi-frame image instances so default to 1
transfer_syntax_uid = try_get_attr(instance_metadata, '00020010')
if transfer_syntax_uid is not None:
transfer_type = transfer_syntax_uid
image_format = None # possible values are None (uncompressed/raw), 'jpeg', or 'jp2'
try:
# start = time.time()
frames = self.client.retrieve_instance_frames(study_instance_uid=self.study_instance_uid,
series_instance_uid=self.series_instance_uid,
sop_instance_uid=self.sop_instance_uid,
frame_numbers=[1],
image_format=image_format)
# stop = time.time()
# print("Took:", stop - start)
except requests.exceptions.HTTPError:
frames = [] # no frames
pass
# need to store frames because Pixbuf.new_from_data doesn't keep own
# reference to data so Python will destroy it without Gtk knowing about it
self.rgb_frames = len(frames)*[None]
for image_index, frame in enumerate(frames):
pixel_count = (self.image_width* self.image_height)
rgb_frame_temp = bytearray(3*pixel_count) # pre-allocate RGB pixels
if image_format is None: # raw pixels
if self.image_bits_per_pixel == 8:
for j in range(0, pixel_count):
gray8 = frame[j]
rgb_frame_temp[3*j + 0] = gray8
rgb_frame_temp[3*j + 1] = gray8
rgb_frame_temp[3*j + 2] = gray8
else:
raise Exception("Cannot handle bits_per_pixel being" +
str(self.image_bits_per_pixel))
# Ref: https://lazka.github.io/pgi-docs/#GdkPixbuf-2.0/classes/Pixbuf.html#GdkPixbuf.Pixbuf.new_from_data
# this better than `Pixbuf.new_from_bytes` which requires
# the data parameter to be wrapped in a GLib.Bytes object
# WARNING: be aware of that `new_from_data` is buggy. See for instance:
# https://stackoverflow.com/questions/29501835/gtk3-gdk-pixbuf-new-from-data-gives-segmentation-fault-core-dumped-error-13
self.rgb_frames[image_index] = bytes(rgb_frame_temp) # bytearray needs to be wrapped in a `bytes` and store here in order to enot loose ref
pixbuf = Pixbuf.new_from_data(self.rgb_frames[image_index], # data
Colorspace.RGB, # colorspace
False, # has_alpha
8, # bits_per_sample
self.image_width, # width
self.image_height, # height
self.image_width*3, # rowstride, 3 because RGB has 3 bytes per pixel
None, None) # rely on Python for deallocation
if image_index == 0:
self.image_view.set_from_pixbuf(pixbuf)
elif image_format == 'jp2':
print("TODO Decode jp2 image...")
if False:
input_stream = Gio.MemoryInputStream.new_from_data(frame, None)
pixbuf = Pixbuf.new_from_stream(input_stream, None)
if image_index == 0:
self.image_view.set_from_pixbuf(pixbuf)
def clear_current_series_store_and_view(self):
self.series_store.clear()
self.series_view.get_selection().unselect_all()
def clear_current_instance_store_and_view(self):
self.instance_store.clear()
self.instance_view.get_selection().unselect_all()
def clear_current_image_and_pixel_data(self):
self.image_view.clear() # remove image before
self.rgb_frames = None # drop pixel data
def study_view_filter_func(self, model, iter, data):
"""Tests if the language in the row is the one in the filter"""
if self.current_study_filter is None or self.current_study_filter == "Any":
return True
else:
return model[iter][2] == self.current_study_filter
def series_view_filter_func(self, model, iter, data):
"""Tests if the language in the row is the one in the filter"""
if self.current_series_filter is None or self.current_series_filter == "Any":
return True
else:
return model[iter][2] == self.current_series_filter
def instance_view_filter_func(self, model, iter, data):
"""Tests if the language in the row is the one in the filter"""
if self.current_instance_filter is None or self.current_instance_filter == "Any":
return True
else:
return model[iter][2] == self.current_instance_filter
if __name__ == '__main__':
win = DICOMWindow()
win.connect("destroy", Gtk.main_quit) # make close button window work
win.show_all() # show everything
Gtk.main() # start event loop