try: # Python 3 import tkinter as tk import tkinter.messagebox as tkm import tkinter.simpledialog as tkd except ImportError: # Python 2 import Tkinter as tk import tkMessageBox as tkm import tkSimpleDialog as tkd import networkx as nx from networkx_viewer.graph_canvas import GraphCanvas from networkx_viewer.tokens import TkPassthroughEdgeToken, TkPassthroughNodeToken from networkx_viewer.autocomplete_entry import AutocompleteEntry class ViewerApp(tk.Tk): """Example simple GUI to plot a NetworkX Graph""" def __init__(self, graph, **kwargs): """Additional keyword arguments beyond graph are passed down to the GraphCanvas. See it's docs for details""" tk.Tk.__init__(self) self.geometry('1000x600') self.title('NetworkX Viewer') bottom_row = 10 self.columnconfigure(0, weight=1) self.rowconfigure(bottom_row, weight=1) self.canvas = GraphCanvas(graph, width=400, height=400, **kwargs) self.canvas.grid(row=0, column=0, rowspan=bottom_row+2, sticky='NESW') self.canvas.onNodeSelected = self.onNodeSelected self.canvas.onEdgeSelected = self.onEdgeSelected r = 0 # Current row tk.Label(self, text='Nodes:').grid(row=r, column=1, sticky='W') self.node_entry = AutocompleteEntry(self.canvas.dataG.nodes) self.node_entry.bind('<Return>',self.add_node, add='+') self.node_entry.bind('<Control-Return>', self.buildNewShortcut, add='+') self.node_entry.grid(row=r, column=2, columnspan=2, sticky='NESW', pady=2) tk.Button(self, text='+', command=self.add_node, width=2).grid( row=r, column=4,sticky=tk.NW,padx=2,pady=2) r += 1 nlsb = tk.Scrollbar(self, orient=tk.VERTICAL) self.node_list = tk.Listbox(self, yscrollcommand=nlsb.set, height=5) self.node_list.grid(row=r, column=1, columnspan=3, sticky='NESW') self.node_list.bind('<Delete>',lambda e: self.node_list.delete(tk.ANCHOR)) nlsb.config(command=self.node_list.yview) nlsb.grid(row=r, column=4, sticky='NWS') r += 1 tk.Label(self, text='Neighbors Levels:').grid(row=r, column=1, columnspan=2, sticky=tk.NW) self.level_entry = tk.Entry(self, width=4) self.level_entry.insert(0,'1') self.level_entry.grid(row=r, column=3, sticky=tk.NW, padx=5) r += 1 tk.Button(self, text='Build New', command=self.onBuildNew).grid( row=r, column=1) tk.Button(self, text='Add to Existing', command=self.onAddToExisting ).grid(row=r, column=2, columnspan=2) r += 1 line = tk.Canvas(self, height=15, width=200) line.create_line(0,13,250,13) line.create_line(0,15,250,15) line.grid(row=r, column=1, columnspan=4, sticky='NESW') r += 1 tk.Label(self, text='Filters:').grid(row=r, column=1, sticky=tk.W) self.filter_entry = tk.Entry(self) self.filter_entry.bind('<Return>',self.add_filter, add='+') self.filter_entry.grid(row=r, column=2, columnspan=2, sticky='NESW', pady=2) tk.Button(self, text='+', command=self.add_filter, width=2).grid( row=r, column=4,sticky=tk.NW,padx=2,pady=2) r += 1 flsb = tk.Scrollbar(self, orient=tk.VERTICAL) self.filter_list = tk.Listbox(self, yscrollcommand=flsb.set, height=5) self.filter_list.grid(row=r, column=1, columnspan=3, sticky='NESW') self.filter_list.bind('<Delete>',self.remove_filter) flsb.config(command=self.node_list.yview) flsb.grid(row=r, column=4, sticky='NWS') r += 1 tk.Button(self, text='Clear',command=self.remove_filter).grid( row=r, column=1, sticky='W') tk.Button(self, text='?', command=self.filter_help ).grid(row=r, column=4, stick='NESW', padx=2) r += 1 line2 = tk.Canvas(self, height=15, width=200) line2.create_line(0,13,250,13) line2.create_line(0,15,250,15) line2.grid(row=r, column=1, columnspan=4, sticky='NESW') r += 1 self.lbl_attr = tk.Label(self, text='Attributes', wraplength=200, anchor=tk.SW, justify=tk.LEFT) self.lbl_attr.grid(row=r, column=1, columnspan=4, sticky='NW') r += 1 self.tbl_attr = PropertyTable(self, {}) self.tbl_attr.grid(row=r, column=1, columnspan=4, sticky='NESW') assert r == bottom_row, "Set bottom_row to %d" % r self._build_menu() def _build_menu(self): self.menubar = tk.Menu(self) self.config(menu=self.menubar) view = tk.Menu(self.menubar, tearoff=0) view.add_command(label='Undo', command=self.canvas.undo, accelerator="Ctrl+Z") self.bind_all("<Control-z>", lambda e: self.canvas.undo()) # Implement accelerator view.add_command(label='Redo', command=self.canvas.redo) view.add_separator() view.add_command(label='Center on node...', command=self.center_on_node) view.add_separator() view.add_command(label='Reset Node Marks', command=self.reset_node_markings) view.add_command(label='Reset Edge Marks', command=self.reset_edge_markings) view.add_command(label='Redraw Plot', command=self.canvas.replot) view.add_separator() view.add_command(label='Grow display one level...', command=self.grow_all) self.menubar.add_cascade(label='View', menu=view) def center_on_node(self): node = NodeDialog(self, "Name of node to center on:").result if node is None: return self.canvas.center_on_node(node) def reset_edge_markings(self): for u,v,k,d in self.canvas.dispG.edges_iter(data=True, keys=True): token = d['token'] if token.is_marked: self.canvas.mark_edge(u,v,k) def reset_node_markings(self): for u,d in self.canvas.dispG.nodes_iter(data=True): token = d['token'] if token.is_marked: self.canvas.mark_node(u) def add_node(self, event=None): node = self.node_entry.get() if node.isdigit() and self.canvas.dataG.has_node(int(node)): node = int(node) if self.canvas.dataG.has_node(node): self.node_list.insert(tk.END, node) self.node_entry.delete(0, tk.END) else: tkm.showerror("Node not found", "Node '%s' not in graph."%node) def add_filter(self, event=None, filter_lambda=None): if filter_lambda is None: filter_lambda = self.filter_entry.get() if self.canvas.add_filter(filter_lambda): # We successfully added the filter; add to list and clear entry self.filter_list.insert(tk.END, filter_lambda) self.filter_entry.delete(0, tk.END) def filter_help(self, event=None): msg = ("Enter a lambda function which returns True if you wish\n" "to show nodes with ONLY a given property.\n" "Parameters are:\n" " - u, the node's name, and \n" " - d, the data dictionary.\n\n" "Example: \n" " d.get('color',None)=='red'\n" "would show only red nodes.\n" "Example 2:\n" " str(u).is_digit()\n" "would show only nodes which have a numerical name.\n\n" "Multiple filters are ANDed together.") tkm.showinfo("Filter Condition", msg) def remove_filter(self, event=None): all_items = self.filter_list.get(0, tk.END) if event is None: # When no event passed, this function was called via the "clear" # button. items = all_items else: # Remove currently selected item items = (self.filter_list.get(tk.ANCHOR),) for item in items: self.canvas.remove_filter(item) idx = all_items.index(item) self.filter_list.delete(idx) all_items = self.filter_list.get(0, tk.END) def grow_all(self): """Grow all visible nodes one level""" for u, d in self.canvas.dispG.node.copy().items(): if not d['token'].is_complete: self.canvas.grow_node(u) def get_node_list(self): """Get nodes in the node list and clear""" # See if we forgot to hit the plus sign if len(self.node_entry.get()) != 0: self.add_node() nodes = self.node_list.get(0, tk.END) self.node_list.delete(0, tk.END) return nodes def onBuildNew(self): nodes = self.get_node_list() if len(nodes) == 2: self.canvas.plot_path(nodes[0], nodes[1], levels=self.level) else: self.canvas.plot(nodes, levels=self.level) def onAddToExisting(self): """Add nodes to existing plot. Prompt to include link to existing if possible""" home_nodes = set(self.get_node_list()) self.canvas.plot_additional(home_nodes, levels=self.level) def buildNewShortcut(self, event=None): # Add node intelligently then doe a build new self.node_entry.event_generate('<Return>') # Resolve current self.onBuildNew() def goto_path(self, event): frm = self.node_entry.get() to = self.node_entry2.get() self.node_entry.delete(0, tk.END) self.node_entry2.delete(0, tk.END) if frm == '': tkm.showerror("No From Node", "Please enter a node in both " "boxes to plot a path. Enter a node in only the first box " "to bring up nodes immediately adjacent.") return if frm.isdigit() and int(frm) in self.canvas.dataG.nodes(): frm = int(frm) if to.isdigit() and int(to) in self.canvas.dataG.nodes(): to = int(to) self.canvas.plot_path(frm, to, levels=self.level) def onNodeSelected(self, node_name, node_dict): self.tbl_attr.build(node_dict) self.lbl_attr.config(text="Attributes of node '%s'"%node_name) def onEdgeSelected(self, edge_name, edge_dict): self.tbl_attr.build(edge_dict) self.lbl_attr.config(text="Attributes of edge between '%s' and '%s'"% edge_name[:2]) @property def level(self): try: l = int(self.level_entry.get()) except ValueError: tkm.showerror("Invalid Level", "Please specify a level between " "greater than or equal to 0") raise return l class TkPassthroughViewerApp(ViewerApp): def __init__(self, graph, **kwargs): ViewerApp.__init__(self, graph, NodeTokenClass=TkPassthroughNodeToken, EdgeTokenClass=TkPassthroughEdgeToken, **kwargs) class PropertyTable(tk.Frame): """A pure Tkinter scrollable frame that actually works! * Use the 'interior' attribute to place widgets inside the scrollable frame * Construct and pack/place/grid normally * This frame only allows vertical scrolling """ def __init__(self, parent, property_dict, *args, **kw): tk.Frame.__init__(self, parent, *args, **kw) # create a canvas object and a vertical scrollbar for scrolling it self.vscrollbar = vscrollbar = tk.Scrollbar(self, orient=tk.VERTICAL) vscrollbar.pack(fill=tk.Y, side=tk.RIGHT, expand=tk.FALSE) self.canvas = canvas = tk.Canvas(self, bd=0, highlightthickness=0, yscrollcommand=vscrollbar.set) canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=tk.TRUE) vscrollbar.config(command=canvas.yview) # reset the view canvas.xview_moveto(0) canvas.yview_moveto(0) # create a frame inside the canvas which will be scrolled with it self.interior = interior = tk.Frame(canvas) self.interior_id = canvas.create_window(0, 0, window=interior, anchor='nw') self.interior.bind('<Configure>', self._configure_interior) self.canvas.bind('<Configure>', self._configure_canvas) self.build(property_dict) def build(self, property_dict): for c in self.interior.winfo_children(): c.destroy() # Filter property dict property_dict = {k: v for k, v in property_dict.items() if self._key_filter_function(k)} # Prettify key/value pairs for display property_dict = {self._make_key_pretty(k): self._make_value_pretty(v) for k, v in property_dict.items()} # Sort by key and filter dict_values = sorted(property_dict.items(), key=lambda x: x[0]) for n,(k,v) in enumerate(dict_values): tk.Label(self.interior, text=k, borderwidth=1, relief=tk.SOLID, wraplength=75, anchor=tk.E, justify=tk.RIGHT).grid( row=n, column=0, sticky='nesw', padx=1, pady=1, ipadx=1) tk.Label(self.interior, text=v, borderwidth=1, wraplength=125, anchor=tk.W, justify=tk.LEFT).grid( row=n, column=1, sticky='nesw', padx=1, pady=1, ipadx=1) def _make_key_pretty(self, key): """Make key of property dictionary displayable Used by build function to make key displayable on the table. Args: key (object) Key of property dictionary from dataG Returns: label (str) String representation of key. Might be made shorter or with different name if desired. """ return str(key) def _make_value_pretty(self, value): """Make key of property dictionary displayable Used by build function to make key displayable on the table. Args: key (object) Key of property dictionary from dataG Returns: label (str) String representation of key. Might be made shorter or with different name if desired. """ label = str(value) if len(label) > 255: label = label[:253] + '...' return label def _key_filter_function(self, key): """Function to determine if key should be displayed. Called by build for each key in the propery dict. Overwrite with your implementation if you want to hide specific keys (all starting "_" for example). Args: key (object) Key of property dictionary from dataG Returns: display (bool) True if the key-value pair associate with this key should be displayed """ # Should be more specifically implemented when subclassed return True # Show all keys def _configure_interior(self, event): """ track changes to the canvas and frame width and sync them, also updating the scrollbar """ # update the scrollbars to match the size of the inner frame size = (self.interior.winfo_reqwidth(), self.interior.winfo_reqheight()) self.canvas.config(scrollregion="0 0 %s %s" % size) if self.interior.winfo_reqwidth() != self.canvas.winfo_width(): # update the canvas's width to fit the inner frame self.canvas.config(width=self.interior.winfo_reqwidth()) def _configure_canvas(self, event): if self.interior.winfo_reqwidth() != self.canvas.winfo_width(): # update the inner frame's width to fill the canvas self.canvas.itemconfigure(self.interior_id, width=self.canvas.winfo_width()) class NodeDialog(tk.Toplevel): def __init__(self, main_window, msg='Please enter a node:'): tk.Toplevel.__init__(self) self.main_window = main_window self.title('Node Entry') self.geometry('170x160') self.rowconfigure(3, weight=1) tk.Label(self, text=msg).grid(row=0, column=0, columnspan=2, sticky='NESW',padx=5,pady=5) self.posibilities = [d['dataG_id'] for n,d in main_window.canvas.dispG.nodes_iter(data=True)] self.entry = AutocompleteEntry(self.posibilities, self) self.entry.bind('<Return>', lambda e: self.destroy(), add='+') self.entry.grid(row=1, column=0, columnspan=2, sticky='NESW',padx=5,pady=5) tk.Button(self, text='Ok', command=self.destroy).grid( row=3, column=0, sticky='ESW',padx=5,pady=5) tk.Button(self, text='Cancel', command=self.cancel).grid( row=3, column=1, sticky='ESW',padx=5,pady=5) # Make modal self.winfo_toplevel().wait_window(self) def destroy(self): res = self.entry.get() if res not in self.posibilities: res = None self.result = res tk.Toplevel.destroy(self) def cancel(self): self.entry.delete(0,tk.END) self.destroy()