9

How to create a new window with a certain dimensions (for example 40% of screen size) using Python API?

Functionality-wise similar to File > User Preferences — a new window shows up with User Preferences area type.

Mikhail Rachinskiy
  • 1,069
  • 8
  • 22

2 Answers2

19

Currently in Blender 2.79 the only way to manage windows through Python API is by bpy.ops.screen.area_dupli() and bpy.ops.wm.window_duplicate() operators.

But they add new screens to bpy.data.screens, which are not easy to remove.

And there is no way to resize created windows.

UPDATE 1:

In my particular case I can use window created by userpref_show operator, it's size is very close to what I need and it creates 'temp' screen which is deleted after window is closed, so I do not need to cleanup bpy.data.screens afterwards:

import bpy

Call user prefs window

bpy.ops.screen.userpref_show("INVOKE_DEFAULT")

Change area type

area = bpy.context.window_manager.windows[-1].screen.areas[0] area.type = "TEXT_EDITOR"

UPDATE 2:

With render.view_show operator it is possible to set the exact size of created window by modifying render settings:

import bpy

Modify render settings

render = bpy.context.scene.render render.resolution_x = 640 render.resolution_y = 480 render.resolution_percentage = 100

Modify preferences (to guaranty new window)

prefs = bpy.context.preferences prefs.view.render_display_type = "WINDOW"

Call image editor window

bpy.ops.render.view_show("INVOKE_DEFAULT")

Change area type

area = bpy.context.window_manager.windows[-1].screen.areas[0] area.type = "TEXT_EDITOR"

Restore render settings and preferences

render.resolution_x = original_value

...

I also restore is_dirty tag which affects preferences autosave feature

prefs.is_dirty = original_value

Mikhail Rachinskiy
  • 1,069
  • 8
  • 22
  • Interesting that it is just as easy to set up buttons in a panel to create editors as popups for specific uses - I made mine a small Image Editor and set it to Paint in the script, and this is actually useful. Thanks. – Craig D Jones Nov 02 '17 at 20:52
  • simple hack but works as expected, I'm looking a way to create multiple of the window, currently this code only allow only one instance of window – aditia Oct 30 '18 at 05:41
  • ah by adding as follow allow me to add a lot of windows with different type
    import bpy
    
    # Call user prefs window
    bpy.ops.screen.userpref_show('INVOKE_DEFAULT')
    
    # Change area type
    area = bpy.context.window_manager.windows[-1].screen.areas[0]
    area.type = 'TEXT_EDITOR'
    bpy.ops.wm.window_duplicate()
    area.type = 'CONSOLE'
    
    – aditia Oct 30 '18 at 05:45
  • @aditia I do not recommend this, each time you use window_duplicate operator it will add additional screen temp.001 that will not be removed after you close the second window, which will mess up your workflow if you switch between screens as often as I do. – Mikhail Rachinskiy Oct 31 '18 at 03:32
  • @MikhailRachinskiy ah I see, you're correct, using dupli make screen messier – aditia Nov 01 '18 at 04:30
6

I have been struggeling with this as well and would like to share my solution. I wanted to be able to open several windows and change their layout in my script. bpy.ops.screen.userpref_show() opens a window with just a single area but it wouldn't allow me to open several windows and the windows would have the title "Blender User Preferences". bpy.ops.wm.window_duplicate() would allow me to open several windows and make them the same size as the current window, but it would open with my multi-area screen and produce a new screen everytime without removing it. This is my solution:

A helper to create contexts for operator calls:

# Construct context
def context(window=None, screen=None, area=None, region=None):
    if window is None: window = C.window
    if screen is None: screen = window.screen
    if area is None: area = screen.areas[0]
    if region is None: region = area.regions[0]
    return {'window': window, 'screen': screen, 'area': area, 'region': region}

Wrappers for Window, Screen and Area:

# Window class wrapper
class Window:
    def __init__(self, window):
        self.window = window

    # Get window screen
    def get_screen(self):
        return Screen(self.window.screen)

    # Create new window
    @classmethod
    def new(cls, screen_name):
        bpy.ops.wm.window_duplicate()
        window = cls(C.window_manager.windows[-1])
        window.get_screen().set_name(screen_name).join_all()
        return window

# Screen class wrapper
class Screen:
    def __init__(self, screen):
        self.screen = screen

    # Get name
    def get_name(self, name):
        return self.screen.name

    # Set name
    def set_name(self, name):
        self.screen.name = name

        # Return self for method chaining
        return self

    # Get scene
    def get_scene(self):
        return self.screen.scene

    # Set scene
    def set_scene(self, scene):
        self.screen.scene = scene

        # Return self for method chaining
        return self

    # Get area
    def area_at(self, index):
        return Area(self.screen.areas[index])

    # Split area and return new one
    def split_area(self, area, dir, factor):
        # Set direction
        if 'vertical'.startswith(dir.lower()): dir = 'VERTICAL'
        elif 'horizontal'.startswith(dir.lower()): dir = 'HORIZONTAL'
        else: raise ValueError("Invalid direction \"%s\""%dir)

        bpy.ops.screen.area_split(
            context(screen=self.screen, area=area.area),
            direction=dir, factor=factor
        )
        return Area(self.screen.areas[-1])

    # Join areas and return success status
    def join_areas(self, area1, area2):
        # Try joining areas
        result = bpy.ops.screen.area_join(context(screen=self.screen),
            min_x=area1.x, min_y=area1.y,
            max_x=area2.x, max_y=area2.y
        )

        # Return eliminated area
        return ('FINISHED' in result)

    # Join all areas
    def join_all(self):
        # Create area index
        index = self.AreaIndex(self.screen.areas)

        # Join all areas
        for i in range(index.count):
            for key in index.x:
                for a1, a2 in index.x_pair_iterator(key):
                    if self.join_areas(a1, a2):
                        index.remove_area(a2)
                        break
            for key in index.y:
                for a1, a2 in index.y_pair_iterator(key):
                    if self.join_areas(a1, a2):
                        index.remove_area(a2)
                        break
            if index.count <= 1: break

        # Return self for method chaining
        return self

    # Area index for area joining
    class AreaIndex:
        # Create area index
        def __init__(self, areas):
            # Create index maps
            self.x = {}
            self.y = {}
            self.count = len(areas)

            # Insert areas into indices
            for area in areas:
                if not area.x in self.x: self.x[area.x] = []
                self.x[area.x].append(area)
                if not area.y in self.y: self.y[area.y] = []
                self.y[area.y].append(area)

            # Sort area lists
            for key in self.x: self.x[key].sort(key=lambda a: a.y)
            for key in self.y: self.y[key].sort(key=lambda a: a.x)

        # Remove area from index
        def remove_area(self, area):
            self.x[area.x].remove(area)
            self.y[area.y].remove(area)
            self.count -= 1

        # Iterate over area pairs for x key
        def x_pair_iterator(self, key):
            for i in range(len(self.x[key]) - 1):
                yield (self.x[key][i], self.x[key][i + 1])

        # Iterate over area pairs for x key
        def y_pair_iterator(self, key):
            for i in range(len(self.y[key]) - 1):
                yield (self.y[key][i], self.y[key][i + 1])

# Area class wrapper
class Area:
    def __init__(self, area):
        self.area = area

    # Get area type
    def get_type(self):
        return self.area.type

    # Set area type
    def set_type(self, type):
        self.area.type = type

        # Return self for method chaining
        return self

Create a window with two areas and remove unused screens:

SCREEN_NAME = "temp"

# Create new window
window = Window.new(SCREEN_NAME)
screen = window.get_screen().set_scene(D.scenes['Test'])

# Setup areas
node_area = screen.area_at(0).set_type('NODE_EDITOR')
movie_area = screen.split_area(node_area, 'v', 0.7).set_type('CLIP_EDITOR')

# Get used screens
used_screens = set(
    w.screen for w in C.window_manager.windows
    if w.screen.name.startswith(SCREEN_NAME)
)

# Delete unused screens
screen_backup = C.window.screen
for s in D.screens:
    if s.name.startswith(SCREEN_NAME) and s not in used_screens:
        bpy.ops.screen.delete(context(screen=s))
C.window.screen = screen_backup
InternetUser
  • 93
  • 1
  • 4