matplotlib工具栏源码探析四(自定义工具项图标)

在matplotlib工具栏源码探析二(添加、删除内置工具项)和matplotlib工具栏源码探析三(添加、删除自定义工具项)两篇文章中,仔细观察会发现,不论内置工具项还是自定义工具项都没有图标,但是默认工具项都有图标!那工具项图标设置的流程是什么呢?

设置内置工具项图标

根据上面列出的两篇文章可知,以pyqt5为后端,与工具栏实现相关的源码为matplotlib.backends.backend_qt5模块的两个类NavigationToolbar2QTToolbarQt。根据NavigationToolbar2QTToolbarQt的源码可知,不管默认的toolbar2模式或者toolmanager模式,工具项的图标都依靠NavigationToolbar2QT类的_icon方法实现。
_icon方法的唯一参数为name,即工具项的名称.png,然后通过cbook._get_data_path('images', name)获取图标文件的实际路径,cbook._get_data_path方法即获取matplotlib的数据目录,Windows下一般为matplotlib安装目录的mpl-data子目录。
打开mpl-data目录的images目录可知,目录中只有默认工具栏中的工具项对应的图标文件,比如前面用到的fullscreen是没有对应图标文件,所以虽然fullscreen等工具项也是内置工具项,但是添加后确是没有图标的。那是不是在在mpl-data\images目录放上工具项对应名称的图标文件就能显示图标了呢?

通过向mpl-data\images目录放置图标文件添加工具项图标

首先将找好的内置quit工具项图标文件quit.png放置到mpl-data\images目录中。然后编写如下代码添加工具项。

使用add_tool方法添加工具项

import matplotlib.pyplot as plt
import matplotlib as mpl

plt.rcParams['toolbar'] = 'toolmanager'
fig = plt.gcf()

fig.canvas.manager.toolbar.add_tool('quit', 'foo')
plt.show()

图标没有出现!
在这里插入图片描述

使用add_toolitem方法添加工具项

import matplotlib.pyplot as plt
import matplotlib as mpl

plt.rcParams['toolbar'] = 'toolmanager'
fig = plt.gcf()

fig.canvas.manager.toolbar.add_toolitem('quit', 'foo', -1,'quit','quit',False) 
plt.show()

图标正常出现!
在这里插入图片描述

结果分析

为什么add_tool方法添加图标失败呢?

查看add_tool方法源码(matplotlib.backend_bases.ToolContainerBase类)可知:
设置图标的关键在于image = self._get_image_filename(tool.image)
查看matplotlib.backend_tools模块工具项相关类定义可知,默认工具栏的工具项对应的类中均定义了image属性,其他内置工具项类未定义image属性,那么就没办法显示工具栏按钮图标,即便放置了对应的图标文件!

为什么add_toolitem方法添加成功呢?

通过源码可知,add_tool方法依赖于add_toolitem添加工具项,直接调用add_toolitem方法就可以直接指定image属性,因此设置成功。

修改内置工具项类设置图标

通过上面分析可知,除默认工具栏的工具项之外的其他内置工具项之所以使用add_tool方法设置图标失败是因为类定义中没有image属性,那么为工具项类添加image属性是否行得通呢?

案例实现

首先先把quit.png放置在mpl-data\images目录中。
代码:

import matplotlib.pyplot as plt
from matplotlib.backend_tools import ToolQuit

plt.rcParams['toolbar'] = 'toolmanager'
#为ToolQuit类增加image属性
ToolQuit.image = 'quit'
fig = plt.gcf()
fig.canvas.manager.toolbar.add_tool('quit', 'foo', -1)

plt.show()

运行后图标未正常显示,经过调试,将quit.png文件名改为quit_large.png,成功!

分析

为什么add_toolitem方法使用quit.png设置成功,而add_tool方法需要使用quit_large.png呢?
经过追踪,发现add_toolitem方法案例中,设置图标文件为quit,经过一系列转换,图标文件的实际路径转换为matplotlib\mpl-data\images\quit,在应用_icon方法设置图标时,因为 name = name.replace('.png', '_large.png')未成功修改文件名,因此未设置quit_large.png为图标文件!关键是matplotlib\mpl-data\images\quit没有扩展名是如何被设置为图标的!因为,QtGui模块是pyd文件,看不到源码,暂时好搞不明白原理。

每次把图标文件放置在mpl-data\images目录中太繁琐,是否能够自定义图标文件路径呢?

设置自定义工具项图标

通过add_toolitem方法和add_toolitem方法设置自定义工具项图标,针对图标文件路径、图标文件名称做了一些实验。

实验一:使用add_tool方法

代码

import matplotlib
matplotlib.rcParams["toolbar"] = "toolmanager"
import matplotlib.pyplot as plt
from matplotlib.backend_tools import ToolBase

class NewTool(ToolBase):
    image = r"C:\quit.png"

fig = plt.figure()
tm = fig.canvas.manager.toolmanager
tm.add_tool("newtool", NewTool)
fig.canvas.manager.toolbar.add_tool("newtool", "toolgroup")
plt.show()

实验分析

在c盘根目录放置quit.pngquit_large.png两个图标文件。

  • 删除c:\quit.png,保留c:\quit_large.png:图标未能正常显示,add_tool方法检测图标文件是否存在,文件不存在无后续操作。
  • 删除c:\quit_large.png,保留c:\quit.png:图标未能正常显示,_icon方法将c:\quit.png转换为c:\quit_large.png,找不到该文件,因此图标未能正常显示。
  • 同时保留quit.pngquit_large.png:图标正常显示。

实验二:使用add_toolitem方法

代码

import matplotlib
matplotlib.rcParams["toolbar"] = "toolmanager"
import matplotlib.pyplot as plt
from matplotlib.backend_tools import ToolBase

class NewTool(ToolBase):
    pass
image = r"c:\quit.png"

fig = plt.figure()
tm = fig.canvas.manager.toolmanager
tm.add_tool("newtool", NewTool)
fig.canvas.manager.toolbar.add_toolitem("newtool", "toolgroup",-1 , image,'test',False)
plt.show()import matplotlib
matplotlib.rcParams["toolbar"] = "toolmanager"
import matplotlib.pyplot as plt
from matplotlib.backend_tools import ToolBase

class NewTool(ToolBase):
    image = r"c:\quit.png"
fig = plt.figure()
tm = fig.canvas.manager.toolmanager
tm.add_tool("newtool", NewTool)
fig.canvas.manager.toolbar.add_toolitem("newtool", "toolgroup",-1 , NewTool.image,'test',False)
plt.show()

实验分析

在c盘根目录放置quit.pngquit_large.png两个图标文件。

  • 删除c:\quit.png,保留c:\quit_large.png:图标正常显示,说明add_toolitem方法并不被检测文件是否存在,接着_icon方法将c:\quit.png转换为c:\quit_large.png,因此图标正常显示。
  • 删除c:\quit_large.png,保留c:\quit.png:图标未能正常显示,_icon方法将c:\quit.png转换为c:\quit_large.png,找不到该文件,因此图标未能正常显示。

实验三:使用add_toolitem方法,去掉图标文件扩展名

代码

import matplotlib
matplotlib.rcParams["toolbar"] = "toolmanager"
import matplotlib.pyplot as plt
from matplotlib.backend_tools import ToolBase

class NewTool(ToolBase):
    pass
image = r"c:\quit"

fig = plt.figure()
tm = fig.canvas.manager.toolmanager
tm.add_tool("newtool", NewTool)
fig.canvas.manager.toolbar.add_toolitem("newtool", "toolgroup",-1 , image,'test',False)
plt.show()

实验四:使用add_toolitem方法,只保留图标文件名称

代码

import matplotlib
matplotlib.rcParams["toolbar"] = "toolmanager"
import matplotlib.pyplot as plt
from matplotlib.backend_tools import ToolBase

class NewTool(ToolBase):
    pass
image = "quit"

fig = plt.figure()
tm = fig.canvas.manager.toolmanager
tm.add_tool("newtool", NewTool)
fig.canvas.manager.toolbar.add_toolitem("newtool", "toolgroup",-1 , image,'test',False)
plt.show()

实验分析

mpl-data\images目录中录放置quit.pngquit_large.png两个图标文件。

  • 删除quit.png,保留quit_large.png:图标未能正常显示。
  • 删除quit_large.png,保留quit.png:图标正常显示。
  • 同时保留quit.pngquit_large.png:图标正常显示。

设置工具项图标总结

  • 无论内置工具项还是自定义工具项,设置图标的关键都是image属性,由于pyqt5使用带_large.png后缀的文件作为图标,因此,最好同时放置两套图标文件。
  • add_toolitem方法和add_toolitem方法都可以设置图标,但是add_toolitem方法会检测文件是否存在,add_toolitem方法不检测文件是否存在,add_tool方法要求两套图标文件同时存在,add_toolitem方法如果设置的图标文件有扩展名只需_large.png文件存在即可。
  • 需要注意的是如果配置的图标文件路径不带扩展名,那么直接使用对应.png作为图标文件,忽略_large.png文件。

matplotlib相关源码

NavigationToolbar2QTToolbarQt类源码

class NavigationToolbar2QT(NavigationToolbar2, QtWidgets.QToolBar):
    # 省略部分代码...
    def __init__(self, canvas, parent, coordinates=True):
        # 省略部分代码...
        for text, tooltip_text, image_file, callback in self.toolitems:
            if text is None:
                self.addSeparator()
            else:
                a = self.addAction(self._icon(image_file + '.png'),
                                   text, getattr(self, callback))
                self._actions[callback] = a
                if callback in ['zoom', 'pan']:
                    a.setCheckable(True)
                if tooltip_text is not None:
                    a.setToolTip(tooltip_text)
                    
    def _icon(self, name):
        """
        Construct a `.QIcon` from an image file *name*, including the extension
        and relative to Matplotlib's "images" data directory.
        """
        if QtCore.qVersion() >= '5.':
            name = name.replace('.png', '_large.png')
        pm = QtGui.QPixmap(str(cbook._get_data_path('images', name)))
        _setDevicePixelRatioF(pm, _devicePixelRatioF(self))
        if self.palette().color(self.backgroundRole()).value() < 128:
            icon_color = self.palette().color(self.foregroundRole())
            mask = pm.createMaskFromColor(QtGui.QColor('black'),
                                        QtCore.Qt.MaskOutColor)
            pm.fill(icon_color)
            pm.setMask(mask)
        return QtGui.QIcon(pm)
class ToolbarQt(ToolContainerBase, QtWidgets.QToolBar):
    # 省略部分代码...
    def add_toolitem(self, name, group, position, image_file, description, toggle):

        button = QtWidgets.QToolButton(self)
        if image_file:
            button.setIcon(NavigationToolbar2QT._icon(self, image_file))
        button.setText(name)
        if description:
            button.setToolTip(description)

        def handler():
            self.trigger_tool(name)
        if toggle:
            button.setCheckable(True)
            button.toggled.connect(handler)
        else:
            button.clicked.connect(handler)

        self._toolitems.setdefault(name, [])
        self._add_to_group(group, name, button, position)
        self._toolitems[name].append((button, handler))

add_tool方法源码

class ToolContainerBase:
    # 省略部分代码
    _icon_extension = '.png'
    def add_tool(self, tool, group, position=-1):
        tool = self.toolmanager.get_tool(tool)
        image = self._get_image_filename(tool.image)
        toggle = getattr(tool, 'toggled', None) is not None
        self.add_toolitem(tool.name, group, position,
                          image, tool.description, toggle)
        if toggle:
            self.toolmanager.toolmanager_connect('tool_trigger_%s' % tool.name,
                                                 self._tool_toggled_cbk)
            # If initially toggled
            if tool.toggled:
                self.toggle_toolitem(tool.name, True)

    def _get_image_filename(self, image):
        """Find the image based on its name."""
        if not image:
            return None

        basedir = cbook._get_data_path("images")
        for fname in [
            image,
            image + self._icon_extension,
            str(basedir / image),
            str(basedir / (image + self._icon_extension)),
        ]:
            if os.path.isfile(fname):
                return fname

ToolBaseSaveFigureBaseToolQuit类源码

class ToolBase:
    image = None
    """
    Filename of the image.

    **String**: Filename of the image to use in the toolbar. If None, the
    *name* is used as a label in the toolbar button.
    """

class SaveFigureBase(ToolBase):
    """Base tool for figure saving."""

    description = 'Save the figure'
    image = 'filesave'
    default_keymap = mpl.rcParams['keymap.save']


class ToolQuit(ToolBase):
    """Tool to call the figure manager destroy method."""

    description = 'Quit the figure'
    default_keymap = mpl.rcParams['keymap.quit']

    def trigger(self, sender, event, data=None):
        Gcf.destroy_fig(self.figure)

你可能感兴趣的:(Matplotlib,matplotlib,工具栏,工具项,图标,源码)